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