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