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