001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Gabriel Barata <gbarata@nuxeo.com>
018 */
019package org.nuxeo.ecm.restapi.server.jaxrs.search;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.servlet.http.HttpServletResponse;
029import javax.ws.rs.core.MultivaluedMap;
030import javax.ws.rs.core.Response;
031
032import org.apache.commons.lang.StringUtils;
033import org.apache.commons.lang3.EnumUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.codehaus.jackson.map.ObjectMapper;
037import org.nuxeo.ecm.automation.core.util.DocumentHelper;
038import org.nuxeo.ecm.automation.core.util.Properties;
039import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
040import org.nuxeo.ecm.automation.server.jaxrs.RestOperationException;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.DocumentModelList;
044import org.nuxeo.ecm.core.api.SortInfo;
045import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
046import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
047import org.nuxeo.ecm.platform.query.api.PageProvider;
048import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
049import org.nuxeo.ecm.platform.query.api.PageProviderService;
050import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider;
051import org.nuxeo.ecm.restapi.server.jaxrs.adapters.SearchAdapter;
052import org.nuxeo.ecm.webengine.model.impl.AbstractResource;
053import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl;
054import org.nuxeo.runtime.api.Framework;
055
056/**
057 * @since 8.3
058 */
059public abstract class QueryExecutor extends AbstractResource<ResourceTypeImpl> {
060
061    public static final String NXQL = "NXQL";
062
063    public static final String QUERY = "query";
064
065    public static final String PAGE_SIZE = "pageSize";
066
067    public static final String CURRENT_PAGE_INDEX = "currentPageIndex";
068
069    public static final String MAX_RESULTS = "maxResults";
070
071    public static final String SORT_BY = "sortBy";
072
073    public static final String SORT_ORDER = "sortOrder";
074
075    public static final String ORDERED_PARAMS = "queryParams";
076
077    public static final String CURRENT_USERID_PATTERN = "$currentUser";
078
079    public static final String CURRENT_REPO_PATTERN = "$currentRepository";
080
081    public enum QueryParams {
082        PAGE_SIZE, CURRENT_PAGE_INDEX, MAX_RESULTS, SORT_BY, SORT_ORDER, ORDERED_PARAMS, QUERY
083    }
084
085    public enum LangParams {
086        NXQL
087    }
088
089    protected PageProviderService pageProviderService;
090
091    private static final Log log = LogFactory.getLog(SearchObject.class);
092
093    public void initExecutor() {
094        pageProviderService = Framework.getService(PageProviderService.class);
095    }
096
097    protected String getQuery(MultivaluedMap<String, String> queryParams) {
098        String query = queryParams.getFirst(QUERY);
099        if (query == null) {
100            query = "SELECT * FROM Document";
101        }
102        return query;
103    }
104
105    protected Long getCurrentPageIndex(MultivaluedMap<String, String> queryParams) {
106        String currentPageIndex = queryParams.getFirst(CURRENT_PAGE_INDEX);
107        if (currentPageIndex != null && !currentPageIndex.isEmpty()) {
108            return Long.valueOf(currentPageIndex);
109        }
110        return null;
111    }
112
113    protected Long getPageSize(MultivaluedMap<String, String> queryParams) {
114        String pageSize = queryParams.getFirst(PAGE_SIZE);
115        if (pageSize != null && !pageSize.isEmpty()) {
116            return Long.valueOf(pageSize);
117        }
118        return null;
119    }
120
121    protected Long getMaxResults(MultivaluedMap<String, String> queryParams) {
122        String maxResults = queryParams.getFirst(MAX_RESULTS);
123        if (maxResults != null && !maxResults.isEmpty()) {
124            return Long.valueOf(maxResults);
125        }
126        return null;
127    }
128
129    protected List<SortInfo> getSortInfo(MultivaluedMap<String, String> queryParams) {
130        String sortBy = queryParams.getFirst(SORT_BY);
131        String sortOrder = queryParams.getFirst(SORT_ORDER);
132        List<SortInfo> sortInfoList = new ArrayList<>();
133        if (!StringUtils.isBlank(sortBy)) {
134            String[] sorts = sortBy.split(",");
135            String[] orders = null;
136            if (!StringUtils.isBlank(sortOrder)) {
137                orders = sortOrder.split(",");
138            }
139            for (int i = 0; i < sorts.length; i++) {
140                String sort = sorts[i];
141                boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
142                sortInfoList.add(new SortInfo(sort, sortAscending));
143            }
144        }
145        return sortInfoList;
146    }
147
148    protected List<SortInfo> getSortInfo(String sortBy, String sortOrder) {
149        List<SortInfo> sortInfoList = new ArrayList<>();
150        if (!StringUtils.isBlank(sortBy)) {
151            String[] sorts = sortBy.split(",");
152            String[] orders = null;
153            if (!StringUtils.isBlank(sortOrder)) {
154                orders = sortOrder.split(",");
155            }
156            for (int i = 0; i < sorts.length; i++) {
157                String sort = sorts[i];
158                boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
159                sortInfoList.add(new SortInfo(sort, sortAscending));
160            }
161        }
162        return sortInfoList;
163    }
164
165    protected Properties getNamedParameters(MultivaluedMap<String, String> queryParams) {
166        Properties namedParameters = new Properties();
167        for (String namedParameterKey : queryParams.keySet()) {
168            if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) {
169                String value = queryParams.getFirst(namedParameterKey);
170                namedParameters.put(namedParameterKey, handleNamedParamVars(value));
171            }
172        }
173        return namedParameters;
174    }
175
176    protected Properties getNamedParameters(Map<String, String> queryParams) {
177        Properties namedParameters = new Properties();
178        for (String namedParameterKey : queryParams.keySet()) {
179            if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) {
180                String value = queryParams.get(namedParameterKey);
181                namedParameters.put(namedParameterKey, handleNamedParamVars(value));
182            }
183        }
184        return namedParameters;
185    }
186
187    protected String handleNamedParamVars(String value) {
188        if (value != null) {
189            if (value.equals(CURRENT_USERID_PATTERN)) {
190                return ctx.getCoreSession().getPrincipal().getName();
191            } else if (value.equals(CURRENT_REPO_PATTERN)) {
192                return ctx.getCoreSession().getRepositoryName();
193            }
194        }
195        return value;
196    }
197
198    protected Object[] getParameters(MultivaluedMap<String, String> queryParams) {
199        List<String> orderedParams = queryParams.get(ORDERED_PARAMS);
200        if (orderedParams != null && !orderedParams.isEmpty()) {
201            Object[] parameters = orderedParams.toArray(new String[orderedParams.size()]);
202            // expand specific parameters
203            replaceParameterPattern(parameters);
204            return parameters;
205        }
206        return null;
207    }
208
209    protected Object[] replaceParameterPattern(Object[] parameters) {
210        for (int idx = 0; idx < parameters.length; idx++) {
211            String value = (String) parameters[idx];
212            if (value.equals(CURRENT_USERID_PATTERN)) {
213                parameters[idx] = ctx.getCoreSession().getPrincipal().getName();
214            } else if (value.equals(CURRENT_REPO_PATTERN)) {
215                parameters[idx] = ctx.getCoreSession().getRepositoryName();
216            }
217        }
218        return parameters;
219    }
220
221    protected Map<String, Serializable> getProperties() {
222        Map<String, Serializable> props = new HashMap<String, Serializable>();
223        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) ctx.getCoreSession());
224        return props;
225    }
226
227    protected DocumentModelList queryByLang(String queryLanguage, MultivaluedMap<String, String> queryParams)
228        throws RestOperationException {
229        if (queryLanguage == null || !EnumUtils.isValidEnum(LangParams.class, queryLanguage)) {
230            throw new RestOperationException("invalid query language", HttpServletResponse.SC_BAD_REQUEST);
231        }
232
233        String query = getQuery(queryParams);
234        Long pageSize = getPageSize(queryParams);
235        Long currentPageIndex = getCurrentPageIndex(queryParams);
236        Long maxResults = getMaxResults(queryParams);
237        Properties namedParameters = getNamedParameters(queryParams);
238        Object[] parameters = getParameters(queryParams);
239        List<SortInfo> sortInfo = getSortInfo(queryParams);
240        Map<String, Serializable> props = getProperties();
241
242        DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService, null,
243            namedParameters);
244
245        return queryByLang(query, pageSize, currentPageIndex, maxResults, sortInfo, parameters, props,
246            searchDocumentModel);
247    }
248
249    protected DocumentModelList queryByPageProvider(String pageProviderName, MultivaluedMap<String, String> queryParams)
250        throws RestOperationException {
251        if (pageProviderName == null) {
252            throw new RestOperationException("invalid page provider name", HttpServletResponse.SC_BAD_REQUEST);
253        }
254
255        Long pageSize = getPageSize(queryParams);
256        Long currentPageIndex = getCurrentPageIndex(queryParams);
257        Properties namedParameters = getNamedParameters(queryParams);
258        Object[] parameters = getParameters(queryParams);
259        List<SortInfo> sortInfo = getSortInfo(queryParams);
260        Map<String, Serializable> props = getProperties();
261
262        DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService,
263            pageProviderName, namedParameters);
264
265        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, sortInfo, parameters, props,
266            searchDocumentModel);
267    }
268
269    protected DocumentModelList queryByLang(String query, Long pageSize, Long currentPageIndex, Long maxResults,
270        List<SortInfo> sortInfo, Object[] parameters, Map<String, Serializable> props,
271        DocumentModel searchDocumentModel) throws RestOperationException {
272        PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition(
273            SearchAdapter.pageProviderName);
274        ppdefinition.setPattern(query);
275        if (maxResults != null && maxResults != -1) {
276            // set the maxResults to avoid slowing down queries
277            ppdefinition.getProperties().put("maxResults", maxResults.toString());
278        }
279        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
280            (PageProvider<DocumentModel>) pageProviderService.getPageProvider(SearchAdapter.pageProviderName,
281                ppdefinition, searchDocumentModel, sortInfo, pageSize, currentPageIndex, props, parameters),
282            null);
283
284        if (res.hasError()) {
285            RestOperationException err = new RestOperationException(res.getErrorMessage());
286            err.setStatus(HttpServletResponse.SC_BAD_REQUEST);
287            throw err;
288        }
289        return res;
290    }
291
292    protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex,
293        List<SortInfo> sortInfo, Object[] parameters, Map<String, Serializable> props,
294        DocumentModel searchDocumentModel) throws RestOperationException {
295        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
296            (PageProvider<DocumentModel>) pageProviderService.getPageProvider(pageProviderName, searchDocumentModel,
297                sortInfo, pageSize, currentPageIndex, props, parameters), null);
298        if (res.hasError()) {
299            RestOperationException err = new RestOperationException(res.getErrorMessage());
300            err.setStatus(HttpServletResponse.SC_BAD_REQUEST);
301            throw err;
302        }
303        return res;
304    }
305
306    protected PageProviderDefinition getPageProviderDefinition(String pageProviderName) throws IOException {
307        return pageProviderService.getPageProviderDefinition(pageProviderName);
308    }
309
310    protected DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps, String providerName,
311        Properties namedParameters) {
312        // generate search document model if type specified on the definition
313        DocumentModel searchDocumentModel = null;
314        if (!StringUtils.isBlank(providerName)) {
315            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
316            if (pageProviderDefinition != null) {
317                String searchDocType = pageProviderDefinition.getSearchDocumentType();
318                if (searchDocType != null) {
319                    searchDocumentModel = session.createDocumentModel(searchDocType);
320                } else if (pageProviderDefinition.getWhereClause() != null) {
321                    // avoid later error on null search doc, in case where clause is only referring to named parameters
322                    // (and no namedParameters are given)
323                    searchDocumentModel = new SimpleDocumentModel();
324                }
325            } else {
326                log.error("No page provider definition found for " + providerName);
327            }
328        }
329
330        if (namedParameters != null && !namedParameters.isEmpty()) {
331            // fall back on simple document if no type defined on page provider
332            if (searchDocumentModel == null) {
333                searchDocumentModel = new SimpleDocumentModel();
334            }
335            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
336                String key = entry.getKey();
337                String value = entry.getValue();
338                try {
339                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
340                } catch (PropertyNotFoundException | IOException e) {
341                    // assume this is a "pure" named parameter, not part of the search doc schema
342                    continue;
343                }
344            }
345            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
346        }
347        return searchDocumentModel;
348    }
349
350    protected Response buildResponse(Response.StatusType status, String type, Object object) throws IOException {
351        ObjectMapper mapper = new ObjectMapper();
352        String message = mapper.writeValueAsString(object);
353        return Response.status(status)
354            .header("Content-Length", message.getBytes("UTF-8").length)
355            .type(type + "; charset=UTF-8")
356            .entity(message)
357            .build();
358    }
359
360}