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    protected PageProviderService pageProviderService;
108
109    protected boolean skipAggregates;
110
111    private static final Log log = LogFactory.getLog(SearchObject.class);
112
113    public void initExecutor() {
114        pageProviderService = Framework.getService(PageProviderService.class);
115        skipAggregates = Boolean.parseBoolean(
116                ctx.getHttpHeaders().getRequestHeaders().getFirst(PageProvider.SKIP_AGGREGATES_PROP));
117    }
118
119    protected String getQuery(MultivaluedMap<String, String> queryParams) {
120        String query = queryParams.getFirst(QUERY);
121        if (query == null) {
122            query = "SELECT * FROM Document";
123        }
124        return query;
125    }
126
127    protected Long getCurrentPageIndex(MultivaluedMap<String, String> queryParams) {
128        String currentPageIndex = queryParams.getFirst(CURRENT_PAGE_INDEX);
129        if (currentPageIndex != null && !currentPageIndex.isEmpty()) {
130            return Long.valueOf(currentPageIndex);
131        }
132        return null;
133    }
134
135    protected Long getCurrentPageOffset(MultivaluedMap<String, String> queryParams) {
136        String currentPageOffset = queryParams.getFirst(CURRENT_PAGE_OFFSET);
137        if (currentPageOffset != null && !currentPageOffset.isEmpty()) {
138            return Long.valueOf(currentPageOffset);
139        }
140        return null;
141    }
142
143    protected Long getPageSize(MultivaluedMap<String, String> queryParams) {
144        String pageSize = queryParams.getFirst(PAGE_SIZE);
145        if (pageSize != null && !pageSize.isEmpty()) {
146            return Long.valueOf(pageSize);
147        }
148        return null;
149    }
150
151    protected Long getMaxResults(MultivaluedMap<String, String> queryParams) {
152        String maxResults = queryParams.getFirst(MAX_RESULTS);
153        if (maxResults != null && !maxResults.isEmpty()) {
154            return Long.valueOf(maxResults);
155        }
156        return null;
157    }
158
159    protected List<SortInfo> getSortInfo(MultivaluedMap<String, String> queryParams) {
160        String sortBy = queryParams.getFirst(SORT_BY);
161        String sortOrder = queryParams.getFirst(SORT_ORDER);
162        return getSortInfo(sortBy, sortOrder);
163    }
164
165    protected List<SortInfo> getSortInfo(String sortBy, String sortOrder) {
166        List<SortInfo> sortInfoList = null;
167        if (!StringUtils.isBlank(sortBy)) {
168            String[] sorts = sortBy.split(",");
169            String[] orders = null;
170            if (!StringUtils.isBlank(sortOrder)) {
171                orders = sortOrder.split(",");
172            }
173            if (sorts.length > 0) {
174                sortInfoList = new ArrayList<>();
175            }
176            for (int i = 0; i < sorts.length; i++) {
177                String sort = sorts[i];
178                boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
179                sortInfoList.add(new SortInfo(sort, sortAscending));
180            }
181        }
182        return sortInfoList;
183    }
184
185    /**
186     * @since 8.4
187     */
188    protected List<QuickFilter> getQuickFilters(String providerName, MultivaluedMap<String, String> queryParams) {
189        PageProviderDefinition pageProviderDefinition = pageProviderService.getPageProviderDefinition(providerName);
190        String quickFilters = queryParams.getFirst(QUICK_FILTERS);
191        List<QuickFilter> quickFilterList = new ArrayList<>();
192        if (!StringUtils.isBlank(quickFilters)) {
193            String[] filters = quickFilters.split(",");
194            List<QuickFilter> ppQuickFilters = pageProviderDefinition.getQuickFilters();
195            for (String filter : filters) {
196                for (QuickFilter quickFilter : ppQuickFilters) {
197                    if (quickFilter.getName().equals(filter)) {
198                        quickFilterList.add(quickFilter);
199                        break;
200                    }
201                }
202            }
203        }
204        return quickFilterList;
205    }
206
207    protected List<String> getHighlights(MultivaluedMap<String, String> queryParams) {
208        String highlight = queryParams.getFirst(HIGHLIGHT);
209        List<String> highlightFields = new ArrayList<>();
210        if (!StringUtils.isBlank(highlight)) {
211            String[] fields = highlight.split(",");
212            highlightFields = Arrays.asList(fields);
213        }
214        return highlightFields;
215    }
216
217    protected Map<String, String> getNamedParameters(MultivaluedMap<String, String> queryParams) {
218        Map<String, String> namedParameters = new HashMap<>();
219        for (String namedParameterKey : queryParams.keySet()) {
220            if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) {
221                String value = queryParams.getFirst(namedParameterKey);
222                namedParameters.put(namedParameterKey, handleNamedParamVars(value));
223            }
224        }
225        return namedParameters;
226    }
227
228    protected Map<String, String> getNamedParameters(Map<String, String> queryParams) {
229        Map<String, String> namedParameters = new HashMap<>();
230        for (String namedParameterKey : queryParams.keySet()) {
231            if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) {
232                String value = queryParams.get(namedParameterKey);
233                namedParameters.put(namedParameterKey, handleNamedParamVars(value));
234            }
235        }
236        return namedParameters;
237    }
238
239    protected String handleNamedParamVars(String value) {
240        if (value != null) {
241            if (value.equals(CURRENT_USERID_PATTERN)) {
242                return ctx.getCoreSession().getPrincipal().getName();
243            } else if (value.equals(CURRENT_REPO_PATTERN)) {
244                return ctx.getCoreSession().getRepositoryName();
245            }
246        }
247        return value;
248    }
249
250    protected Object[] getParameters(MultivaluedMap<String, String> queryParams) {
251        List<String> orderedParams = queryParams.get(ORDERED_PARAMS);
252        if (orderedParams != null && !orderedParams.isEmpty()) {
253            Object[] parameters = orderedParams.toArray(new String[orderedParams.size()]);
254            // expand specific parameters
255            replaceParameterPattern(parameters);
256            return parameters;
257        }
258        return null;
259    }
260
261    protected Object[] replaceParameterPattern(Object[] parameters) {
262        for (int idx = 0; idx < parameters.length; idx++) {
263            String value = (String) parameters[idx];
264            if (value.equals(CURRENT_USERID_PATTERN)) {
265                parameters[idx] = ctx.getCoreSession().getPrincipal().getName();
266            } else if (value.equals(CURRENT_REPO_PATTERN)) {
267                parameters[idx] = ctx.getCoreSession().getRepositoryName();
268            }
269        }
270        return parameters;
271    }
272
273    protected Map<String, Serializable> getProperties() {
274        Map<String, Serializable> props = new HashMap<>();
275        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) ctx.getCoreSession());
276        props.put(PageProvider.SKIP_AGGREGATES_PROP, skipAggregates);
277        return props;
278    }
279
280    protected DocumentModelList queryByLang(String queryLanguage, MultivaluedMap<String, String> queryParams) {
281        if (queryLanguage == null || !EnumUtils.isValidEnum(LangParams.class, queryLanguage)) {
282            throw new IllegalParameterException("invalid query language");
283        }
284        return queryByLang(queryParams);
285    }
286
287    protected DocumentModelList queryByLang(MultivaluedMap<String, String> queryParams) {
288        String query = getQuery(queryParams);
289        Long pageSize = getPageSize(queryParams);
290        Long currentPageIndex = getCurrentPageIndex(queryParams);
291        Long currentPageOffset = getCurrentPageOffset(queryParams);
292        Long maxResults = getMaxResults(queryParams);
293        Map<String, String> namedParameters = getNamedParameters(queryParams);
294        Object[] parameters = getParameters(queryParams);
295        List<SortInfo> sortInfo = getSortInfo(queryParams);
296        Map<String, Serializable> props = getProperties();
297
298        DocumentModel searchDocumentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(), null,
299                namedParameters);
300
301        return queryByLang(query, pageSize, currentPageIndex, currentPageOffset, maxResults, sortInfo,
302                props, searchDocumentModel, parameters);
303    }
304
305    protected DocumentModelList queryByPageProvider(String pageProviderName,
306            MultivaluedMap<String, String> queryParams) {
307        if (pageProviderName == null) {
308            throw new IllegalParameterException("invalid page provider name");
309        }
310
311        Long pageSize = getPageSize(queryParams);
312        Long currentPageIndex = getCurrentPageIndex(queryParams);
313        Long currentPageOffset = getCurrentPageOffset(queryParams);
314        Map<String, String> namedParameters = getNamedParameters(queryParams);
315        Object[] parameters = getParameters(queryParams);
316        List<SortInfo> sortInfo = getSortInfo(queryParams);
317        List<QuickFilter> quickFilters = getQuickFilters(pageProviderName, queryParams);
318        List<String> highlights = getHighlights(queryParams);
319        Map<String, Serializable> props = getProperties();
320
321        DocumentModel searchDocumentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(),
322                pageProviderName, namedParameters);
323
324        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo,
325                highlights, quickFilters, props, searchDocumentModel, parameters);
326    }
327
328    @SuppressWarnings("unchecked")
329    protected DocumentModelList queryByLang(String query, Long pageSize, Long currentPageIndex, Long currentPageOffset,
330            Long maxResults, List<SortInfo> sortInfo, Map<String, Serializable> props,
331            DocumentModel searchDocumentModel, Object... parameters) {
332        PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition(
333                SearchAdapter.pageProviderName);
334        ppdefinition.setPattern(query);
335        if (maxResults != null && maxResults != -1) {
336            // set the maxResults to avoid slowing down queries
337            ppdefinition.getProperties().put("maxResults", maxResults.toString());
338        }
339        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
340                (PageProvider<DocumentModel>) pageProviderService.getPageProvider(SearchAdapter.pageProviderName,
341                        ppdefinition, searchDocumentModel, sortInfo, pageSize, currentPageIndex, currentPageOffset,
342                        props, null, null, parameters),
343                null);
344        if (res.hasError()) {
345            throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST);
346        }
347        return res;
348    }
349
350    /**
351     * @since 8.4
352     */
353    protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex,
354            Long currentPageOffset, List<SortInfo> sortInfo, List<QuickFilter> quickFilters, Object[] parameters,
355            Map<String, Serializable> props, DocumentModel searchDocumentModel) {
356        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo, null,
357                quickFilters, props, searchDocumentModel, parameters);
358    }
359
360    @SuppressWarnings("unchecked")
361    protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex,
362            Long currentPageOffset, List<SortInfo> sortInfo, List<String> highlights, List<QuickFilter> quickFilters,
363            Map<String, Serializable> props, DocumentModel searchDocumentModel, Object... parameters) {
364        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
365                (PageProvider<DocumentModel>) pageProviderService.getPageProvider(pageProviderName, searchDocumentModel,
366                        sortInfo, pageSize, currentPageIndex, currentPageOffset, props, highlights, quickFilters,
367                        parameters),
368                null);
369        if (res.hasError()) {
370            throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST);
371        }
372        return res;
373    }
374
375    protected PageProviderDefinition getPageProviderDefinition(String pageProviderName) {
376        return pageProviderService.getPageProviderDefinition(pageProviderName);
377    }
378
379    protected Response buildResponse(Response.StatusType status, String type, Object object) throws IOException {
380        ObjectMapper mapper = new ObjectMapper();
381        String message = mapper.writeValueAsString(object);
382        return Response.status(status)
383                       .header("Content-Length", message.getBytes("UTF-8").length)
384                       .type(type + "; charset=UTF-8")
385                       .entity(message)
386                       .build();
387    }
388
389    protected List<String> asStringList(String value) {
390        return StringUtils.isBlank(value) ? null : Arrays.asList(value.split(","));
391    }
392}