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