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