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.StringUtils;
035import org.apache.commons.lang3.EnumUtils;
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    private static final Log log = LogFactory.getLog(SearchObject.class);
114
115    public void initExecutor() {
116        pageProviderService = Framework.getService(PageProviderService.class);
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 Properties getNamedParameters(MultivaluedMap<String, String> queryParams) {
218        Properties namedParameters = new Properties();
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 Properties getNamedParameters(Map<String, String> queryParams) {
229        Properties namedParameters = new Properties();
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        return props;
277    }
278
279    protected DocumentModelList queryByLang(String queryLanguage, MultivaluedMap<String, String> queryParams) {
280        if (queryLanguage == null || !EnumUtils.isValidEnum(LangParams.class, queryLanguage)) {
281            throw new IllegalParameterException("invalid query language");
282        }
283
284        String query = getQuery(queryParams);
285        Long pageSize = getPageSize(queryParams);
286        Long currentPageIndex = getCurrentPageIndex(queryParams);
287        Long currentPageOffset = getCurrentPageOffset(queryParams);
288        Long maxResults = getMaxResults(queryParams);
289        Properties namedParameters = getNamedParameters(queryParams);
290        Object[] parameters = getParameters(queryParams);
291        List<SortInfo> sortInfo = getSortInfo(queryParams);
292        Map<String, Serializable> props = getProperties();
293
294        DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService, null,
295                namedParameters);
296
297        return queryByLang(query, pageSize, currentPageIndex, currentPageOffset, maxResults, sortInfo, parameters,
298                props, searchDocumentModel);
299    }
300
301    protected DocumentModelList queryByPageProvider(String pageProviderName,
302            MultivaluedMap<String, String> queryParams) {
303        if (pageProviderName == null) {
304            throw new IllegalParameterException("invalid page provider name");
305        }
306
307        Long pageSize = getPageSize(queryParams);
308        Long currentPageIndex = getCurrentPageIndex(queryParams);
309        Long currentPageOffset = getCurrentPageOffset(queryParams);
310        Properties namedParameters = getNamedParameters(queryParams);
311        Object[] parameters = getParameters(queryParams);
312        List<SortInfo> sortInfo = getSortInfo(queryParams);
313        List<QuickFilter> quickFilters = getQuickFilters(pageProviderName, queryParams);
314        List<String> highlights = getHighlights(queryParams);
315        Map<String, Serializable> props = getProperties();
316
317        DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService,
318                pageProviderName, namedParameters);
319
320        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo,
321                highlights, quickFilters, parameters, props, searchDocumentModel);
322    }
323
324    @SuppressWarnings("unchecked")
325    protected DocumentModelList queryByLang(String query, Long pageSize, Long currentPageIndex, Long currentPageOffset,
326            Long maxResults, List<SortInfo> sortInfo, Object[] parameters, Map<String, Serializable> props,
327            DocumentModel searchDocumentModel) {
328        PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition(
329                SearchAdapter.pageProviderName);
330        ppdefinition.setPattern(query);
331        if (maxResults != null && maxResults != -1) {
332            // set the maxResults to avoid slowing down queries
333            ppdefinition.getProperties().put("maxResults", maxResults.toString());
334        }
335        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
336                (PageProvider<DocumentModel>) pageProviderService.getPageProvider(SearchAdapter.pageProviderName,
337                        ppdefinition, searchDocumentModel, sortInfo, pageSize, currentPageIndex, currentPageOffset,
338                        props, null, null, parameters),
339                null);
340        if (res.hasError()) {
341            throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST);
342        }
343        return res;
344    }
345
346    /**
347     * @since 8.4
348     */
349    protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex,
350            Long currentPageOffset, List<SortInfo> sortInfo, List<QuickFilter> quickFilters, Object[] parameters,
351            Map<String, Serializable> props, DocumentModel searchDocumentModel) {
352        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo, null,
353                quickFilters, parameters, props, searchDocumentModel);
354    }
355
356    @SuppressWarnings("unchecked")
357    protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex,
358            Long currentPageOffset, List<SortInfo> sortInfo, List<String> highlights, List<QuickFilter> quickFilters,
359            Object[] parameters, Map<String, Serializable> props, DocumentModel searchDocumentModel) {
360        PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl(
361                (PageProvider<DocumentModel>) pageProviderService.getPageProvider(pageProviderName, searchDocumentModel,
362                        sortInfo, pageSize, currentPageIndex, currentPageOffset, props, highlights, quickFilters,
363                        parameters),
364                null);
365        if (res.hasError()) {
366            throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST);
367        }
368        return res;
369    }
370
371    protected PageProviderDefinition getPageProviderDefinition(String pageProviderName) {
372        return pageProviderService.getPageProviderDefinition(pageProviderName);
373    }
374
375    protected DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps, String providerName,
376            Properties namedParameters) {
377        // generate search document model if type specified on the definition
378        DocumentModel searchDocumentModel = null;
379        if (!StringUtils.isBlank(providerName)) {
380            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
381            if (pageProviderDefinition != null) {
382                String searchDocType = pageProviderDefinition.getSearchDocumentType();
383                if (searchDocType != null) {
384                    searchDocumentModel = session.createDocumentModel(searchDocType);
385                } else if (pageProviderDefinition.getWhereClause() != null) {
386                    // avoid later error on null search doc, in case where clause is only referring to named parameters
387                    // (and no namedParameters are given)
388                    searchDocumentModel = new SimpleDocumentModel();
389                }
390            } else {
391                log.error("No page provider definition found for " + providerName);
392            }
393        }
394
395        if (namedParameters != null && !namedParameters.isEmpty()) {
396            // fall back on simple document if no type defined on page provider
397            if (searchDocumentModel == null) {
398                searchDocumentModel = new SimpleDocumentModel();
399            }
400            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
401                String key = entry.getKey();
402                String value = entry.getValue();
403                try {
404                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
405                } catch (PropertyNotFoundException | IOException e) {
406                    // assume this is a "pure" named parameter, not part of the search doc schema
407                    continue;
408                }
409            }
410            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
411        }
412        return searchDocumentModel;
413    }
414
415    protected Response buildResponse(Response.StatusType status, String type, Object object) throws IOException {
416        ObjectMapper mapper = new ObjectMapper();
417        String message = mapper.writeValueAsString(object);
418        return Response.status(status)
419                       .header("Content-Length", message.getBytes("UTF-8").length)
420                       .type(type + "; charset=UTF-8")
421                       .entity(message)
422                       .build();
423    }
424
425}