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