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