001/*
002 * (C) Copyright 2014 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 *     Vladimir Pasquier <vpasquier@nuxeo.com>
018 */
019package org.nuxeo.ecm.restapi.server.jaxrs;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.EnumMap;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.servlet.http.HttpServletResponse;
030import javax.ws.rs.GET;
031import javax.ws.rs.Path;
032import javax.ws.rs.PathParam;
033import javax.ws.rs.core.Context;
034import javax.ws.rs.core.MultivaluedMap;
035import javax.ws.rs.core.UriInfo;
036
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.nuxeo.ecm.automation.core.util.DocumentHelper;
041import org.nuxeo.ecm.automation.core.util.Properties;
042import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl;
043import org.nuxeo.ecm.automation.server.jaxrs.RestOperationException;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentModelList;
047import org.nuxeo.ecm.core.api.SortInfo;
048import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
049import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
050import org.nuxeo.ecm.platform.query.api.PageProvider;
051import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
052import org.nuxeo.ecm.platform.query.api.PageProviderService;
053import org.nuxeo.ecm.platform.query.api.QuickFilter;
054import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider;
055import org.nuxeo.ecm.restapi.server.jaxrs.adapters.SearchAdapter;
056import org.nuxeo.ecm.webengine.model.WebObject;
057import org.nuxeo.ecm.webengine.model.impl.AbstractResource;
058import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl;
059import org.nuxeo.runtime.api.Framework;
060
061/**
062 * @since 6.0 Search endpoint to perform queries on the repository through rest api.
063 */
064@WebObject(type = "query")
065public class QueryObject extends AbstractResource<ResourceTypeImpl> {
066
067    public static final String PATH = "query";
068
069    public static final String NXQL = "NXQL";
070
071    public static final String QUERY = "query";
072
073    public static final String PAGE_SIZE = "pageSize";
074
075    public static final String CURRENT_PAGE_INDEX = "currentPageIndex";
076
077    public static final String MAX_RESULTS = "maxResults";
078
079    public static final String SORT_BY = "sortBy";
080
081    public static final String SORT_ORDER = "sortOrder";
082
083    public static final String ORDERED_PARAMS = "queryParams";
084
085    public static final String CURRENT_USERID_PATTERN = "$currentUser";
086
087    public static final String CURRENT_REPO_PATTERN = "$currentRepository";
088
089    /**
090     * @since 8.4
091     */
092    public static final String QUICK_FILTERS = "quickFilters";
093
094    private static final Log log = LogFactory.getLog(QueryObject.class);
095
096    protected EnumMap<QueryParams, String> queryParametersMap;
097
098    protected EnumMap<LangParams, String> langPathMap;
099
100    protected PageProviderService pageProviderService;
101
102    @Override
103    public void initialize(Object... args) {
104        pageProviderService = Framework.getLocalService(PageProviderService.class);
105        // Query Enum Parameters Map
106        queryParametersMap = new EnumMap<>(QueryParams.class);
107        queryParametersMap.put(QueryParams.PAGE_SIZE, PAGE_SIZE);
108        queryParametersMap.put(QueryParams.CURRENT_PAGE_INDEX, CURRENT_PAGE_INDEX);
109        queryParametersMap.put(QueryParams.MAX_RESULTS, MAX_RESULTS);
110        queryParametersMap.put(QueryParams.SORT_BY, SORT_BY);
111        queryParametersMap.put(QueryParams.SORT_ORDER, SORT_ORDER);
112        queryParametersMap.put(QueryParams.QUERY, QUERY);
113        queryParametersMap.put(QueryParams.ORDERED_PARAMS, ORDERED_PARAMS);
114        queryParametersMap.put(QueryParams.QUICK_FILTERS, QUICK_FILTERS);
115        // Lang Path Enum Map
116        langPathMap = new EnumMap<>(LangParams.class);
117        langPathMap.put(LangParams.NXQL, NXQL);
118    }
119
120    @SuppressWarnings("unchecked")
121    protected DocumentModelList getQuery(UriInfo uriInfo, String langOrProviderName) throws RestOperationException {
122        // Fetching all parameters
123        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
124        // Look if provider name is given
125        String providerName = null;
126        if (!langPathMap.containsValue(langOrProviderName)) {
127            providerName = langOrProviderName;
128        }
129        String query = queryParams.getFirst(QUERY);
130        String pageSize = queryParams.getFirst(PAGE_SIZE);
131        String currentPageIndex = queryParams.getFirst(CURRENT_PAGE_INDEX);
132        String maxResults = queryParams.getFirst(MAX_RESULTS);
133        String sortBy = queryParams.getFirst(SORT_BY);
134        String sortOrder = queryParams.getFirst(SORT_ORDER);
135        List<String> orderedParams = queryParams.get(ORDERED_PARAMS);
136        String quickFilters = queryParams.getFirst(QUICK_FILTERS);
137
138        // If no query or provider name has been found
139        // Execute big select
140        if (query == null && StringUtils.isBlank(providerName)) {
141            // provide a defaut query
142            query = "SELECT * from Document";
143        }
144
145        // Fetching named parameters (other custom query parameters in the
146        // path)
147        Properties namedParameters = new Properties();
148        for (String namedParameterKey : queryParams.keySet()) {
149            if (!queryParametersMap.containsValue(namedParameterKey)) {
150                String value = queryParams.getFirst(namedParameterKey);
151                if (value != null) {
152                    if (value.equals(CURRENT_USERID_PATTERN)) {
153                        value = ctx.getCoreSession().getPrincipal().getName();
154                    } else if (value.equals(CURRENT_REPO_PATTERN)) {
155                        value = ctx.getCoreSession().getRepositoryName();
156                    }
157                }
158                namedParameters.put(namedParameterKey, value);
159            }
160        }
161
162        // Target query page
163        Long targetPage = null;
164        if (currentPageIndex != null) {
165            targetPage = Long.valueOf(currentPageIndex);
166        }
167
168        // Target page size
169        Long targetPageSize = null;
170        if (pageSize != null) {
171            targetPageSize = Long.valueOf(pageSize);
172        }
173
174        // Ordered Parameters
175        Object[] parameters = null;
176        if (orderedParams != null && !orderedParams.isEmpty()) {
177            parameters = orderedParams.toArray(new String[orderedParams.size()]);
178            // expand specific parameters
179            for (int idx = 0; idx < parameters.length; idx++) {
180                String value = (String) parameters[idx];
181                if (value.equals(CURRENT_USERID_PATTERN)) {
182                    parameters[idx] = ctx.getCoreSession().getPrincipal().getName();
183                } else if (value.equals(CURRENT_REPO_PATTERN)) {
184                    parameters[idx] = ctx.getCoreSession().getRepositoryName();
185                }
186            }
187        }
188
189        Map<String, Serializable> props = new HashMap<String, Serializable>();
190        props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) ctx.getCoreSession());
191
192        DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService,
193                providerName, namedParameters);
194
195        // Sort Info Management
196        List<SortInfo> sortInfoList = null;
197        if (!StringUtils.isBlank(sortBy)) {
198            sortInfoList = new ArrayList<>();
199            String[] sorts = sortBy.split(",");
200            String[] orders = null;
201            if (!StringUtils.isBlank(sortOrder)) {
202                orders = sortOrder.split(",");
203            }
204            for (int i = 0; i < sorts.length; i++) {
205                String sort = sorts[i];
206                boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase()));
207                sortInfoList.add(new SortInfo(sort, sortAscending));
208            }
209        }
210
211        PaginableDocumentModelListImpl res;
212        if (query != null) {
213            PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition(
214                    SearchAdapter.pageProviderName);
215            ppdefinition.setPattern(query);
216            if (maxResults != null && !maxResults.isEmpty() && !maxResults.equals("-1")) {
217                // set the maxResults to avoid slowing down queries
218                ppdefinition.getProperties().put("maxResults", maxResults);
219            }
220            if (StringUtils.isBlank(providerName)) {
221                providerName = SearchAdapter.pageProviderName;
222            }
223
224            res = new PaginableDocumentModelListImpl(
225                    (PageProvider<DocumentModel>) pageProviderService.getPageProvider(providerName, ppdefinition,
226                            searchDocumentModel, sortInfoList, targetPageSize, targetPage, props, parameters),
227                    null);
228        } else {
229            PageProviderDefinition pageProviderDefinition = pageProviderService.getPageProviderDefinition(providerName);
230            // Quick filters management
231            List<QuickFilter> quickFilterList = new ArrayList<>();
232            if (quickFilters != null && !quickFilters.isEmpty()) {
233                String[] filters = quickFilters.split(",");
234                List<QuickFilter> ppQuickFilters = pageProviderDefinition.getQuickFilters();
235                for (String filter : filters) {
236                    for (QuickFilter quickFilter : ppQuickFilters) {
237                        if (quickFilter.getName().equals(filter)) {
238                            quickFilterList.add(quickFilter);
239                            break;
240                        }
241                    }
242                }
243            }
244            res = new PaginableDocumentModelListImpl(
245                    (PageProvider<DocumentModel>) pageProviderService.getPageProvider(providerName, searchDocumentModel,
246                            sortInfoList, targetPageSize, targetPage, props, quickFilterList, parameters),
247                    null);
248        }
249        if (res.hasError()) {
250            RestOperationException err = new RestOperationException(res.getErrorMessage());
251            err.setStatus(HttpServletResponse.SC_BAD_REQUEST);
252            throw err;
253        }
254        return res;
255    }
256
257    protected DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps, String providerName,
258            Properties namedParameters) {
259        // generate search document model if type specified on the definition
260        DocumentModel searchDocumentModel = null;
261        if (!StringUtils.isBlank(providerName)) {
262            PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName);
263            if (pageProviderDefinition != null) {
264                String searchDocType = pageProviderDefinition.getSearchDocumentType();
265                if (searchDocType != null) {
266                    searchDocumentModel = session.createDocumentModel(searchDocType);
267                } else if (pageProviderDefinition.getWhereClause() != null) {
268                    // avoid later error on null search doc, in case where clause is only referring to named parameters
269                    // (and no namedParameters are given)
270                    searchDocumentModel = new SimpleDocumentModel();
271                }
272            } else {
273                log.error("No page provider definition found for " + providerName);
274            }
275        }
276
277        if (namedParameters != null && !namedParameters.isEmpty()) {
278            // fall back on simple document if no type defined on page provider
279            if (searchDocumentModel == null) {
280                searchDocumentModel = new SimpleDocumentModel();
281            }
282            for (Map.Entry<String, String> entry : namedParameters.entrySet()) {
283                String key = entry.getKey();
284                String value = entry.getValue();
285                try {
286                    DocumentHelper.setProperty(session, searchDocumentModel, key, value, true);
287                } catch (PropertyNotFoundException | IOException e) {
288                    // assume this is a "pure" named parameter, not part of the search doc schema
289                    continue;
290                }
291            }
292            searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters);
293        }
294        return searchDocumentModel;
295    }
296
297    /**
298     * Perform query on the repository. By default in NXQL.
299     *
300     * @param uriInfo Query parameters
301     * @return Document Listing
302     */
303    @GET
304    public Object doQuery(@Context UriInfo uriInfo) throws RestOperationException {
305        return getQuery(uriInfo, NXQL);
306    }
307
308    /**
309     * Perform query on the repository in NXQL or specific pageprovider name
310     *
311     * @param uriInfo Query parameters
312     * @param langOrProviderName NXQL or specific provider name
313     * @return Document Listing
314     */
315    @GET
316    @Path("{langOrProviderName}")
317    public Object doSpecificQuery(@Context UriInfo uriInfo, @PathParam("langOrProviderName") String langOrProviderName)
318            throws RestOperationException {
319        return getQuery(uriInfo, langOrProviderName);
320    }
321
322    public enum QueryParams {
323        PAGE_SIZE, CURRENT_PAGE_INDEX, MAX_RESULTS, SORT_BY, SORT_ORDER, ORDERED_PARAMS, QUERY, QUICK_FILTERS
324    }
325
326    public enum LangParams {
327        NXQL
328    }
329
330}