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