001/*
002 * (C) Copyright 2016-2018 Nuxeo (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 static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
022
023import java.io.IOException;
024import java.io.Serializable;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import javax.ws.rs.Consumes;
032import javax.ws.rs.DELETE;
033import javax.ws.rs.GET;
034import javax.ws.rs.POST;
035import javax.ws.rs.PUT;
036import javax.ws.rs.Path;
037import javax.ws.rs.PathParam;
038import javax.ws.rs.core.Context;
039import javax.ws.rs.core.MediaType;
040import javax.ws.rs.core.MultivaluedMap;
041import javax.ws.rs.core.Response;
042import javax.ws.rs.core.UriInfo;
043
044import org.apache.commons.lang3.EnumUtils;
045import org.apache.commons.lang3.StringUtils;
046import org.nuxeo.ecm.automation.core.util.DocumentHelper;
047import org.nuxeo.ecm.automation.core.util.PageProviderHelper;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.DocumentModelList;
050import org.nuxeo.ecm.core.api.NuxeoException;
051import org.nuxeo.ecm.core.api.SortInfo;
052import org.nuxeo.ecm.core.api.model.Property;
053import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
054import org.nuxeo.ecm.platform.query.api.PageProvider;
055import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
056import org.nuxeo.ecm.platform.query.api.PageProviderService;
057import org.nuxeo.ecm.platform.query.api.QuickFilter;
058import org.nuxeo.ecm.platform.search.core.InvalidSearchParameterException;
059import org.nuxeo.ecm.platform.search.core.SavedSearch;
060import org.nuxeo.ecm.platform.search.core.SavedSearchConstants;
061import org.nuxeo.ecm.platform.search.core.SavedSearchRequest;
062import org.nuxeo.ecm.platform.search.core.SavedSearchService;
063import org.nuxeo.ecm.webengine.model.WebObject;
064import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException;
065import org.nuxeo.runtime.api.Framework;
066
067/**
068 * @since 8.3 Search endpoint to perform queries via rest api, with support to save and execute saved search queries.
069 */
070@WebObject(type = "search")
071public class SearchObject extends QueryExecutor {
072
073    public static final String SAVED_SEARCHES_PAGE_PROVIDER = "SAVED_SEARCHES_ALL";
074
075    public static final String SAVED_SEARCHES_PAGE_PROVIDER_PARAMS = "SAVED_SEARCHES_ALL_PAGE_PROVIDER";
076
077    public static final String PAGE_PROVIDER_NAME_PARAM = "pageProvider";
078
079    protected SavedSearchService savedSearchService;
080
081    @Override
082    public void initialize(Object... args) {
083        initExecutor();
084        savedSearchService = Framework.getService(SavedSearchService.class);
085    }
086
087    /**
088     * @deprecated since 10.3, use {@link #doQueryByLang(UriInfo)} instead.
089     */
090    @GET
091    @Path("lang/{queryLanguage}/execute")
092    @Deprecated
093    public Object doQueryByLang(@Context UriInfo uriInfo, @PathParam("queryLanguage") String queryLanguage) {
094        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
095        return queryByLang(queryLanguage, queryParams);
096    }
097
098    /**
099     * @since 10.3
100     */
101    @GET
102    @Path("execute")
103    public Object doQueryByLang(@Context UriInfo uriInfo) {
104        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
105        return queryByLang(queryParams);
106    }
107
108    /**
109     * @deprecated since 10.3, use {@link #doBulkActionByLang(UriInfo)} instead.
110     */
111    @Path("lang/{queryLanguage}/bulk")
112    @Deprecated
113    public Object doBulkActionByLang(@Context UriInfo uriInfo, @PathParam("queryLanguage") String queryLanguage) {
114        if (!EnumUtils.isValidEnum(LangParams.class, queryLanguage)) {
115            throw new IllegalParameterException("invalid query language");
116        }
117        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
118        String query = getQueryString(null, queryParams);
119        return newObject("bulkAction", query);
120    }
121
122    /**
123     * @since 10.3
124     */
125    @Path("bulk")
126    public Object doBulkActionByLang(@Context UriInfo uriInfo) {
127        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
128        String query = getQueryString(null, queryParams);
129        return newObject("bulkAction", query);
130    }
131
132    @GET
133    @Path("pp/{pageProviderName}/execute")
134    public Object doQueryByPageProvider(@Context UriInfo uriInfo,
135            @PathParam("pageProviderName") String pageProviderName) {
136        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
137        return queryByPageProvider(pageProviderName, queryParams);
138    }
139
140    @GET
141    @Path("pp/{pageProviderName}")
142    public Object doGetPageProviderDefinition(@PathParam("pageProviderName") String pageProviderName)
143            throws IOException {
144        return buildResponse(Response.Status.OK, MediaType.APPLICATION_JSON,
145                getPageProviderDefinition(pageProviderName));
146    }
147
148    @Path("pp/{pageProviderName}/bulk")
149    public Object doBulkActionByPageProvider(@PathParam("pageProviderName") String pageProviderName,
150            @Context UriInfo uriInfo) {
151        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
152        String query = getQueryString(pageProviderName, queryParams);
153        return newObject("bulkAction", query);
154    }
155
156    @GET
157    @Path("saved")
158    public List<SavedSearch> doGetSavedSearches(@Context UriInfo uriInfo) {
159        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
160        DocumentModelList results = queryParams.containsKey(PAGE_PROVIDER_NAME_PARAM)
161                ? queryByPageProvider(SAVED_SEARCHES_PAGE_PROVIDER_PARAMS, queryParams)
162                : queryByPageProvider(SAVED_SEARCHES_PAGE_PROVIDER, queryParams);
163        List<SavedSearch> savedSearches = new ArrayList<>(results.size());
164        for (DocumentModel doc : results) {
165            savedSearches.add(doc.getAdapter(SavedSearch.class));
166        }
167        return savedSearches;
168    }
169
170    @POST
171    @Path("saved")
172    @Consumes(MediaType.APPLICATION_JSON)
173    public Response doSaveSearch(SavedSearchRequest request) {
174        try {
175            SavedSearch search = savedSearchService.createSavedSearch(ctx.getCoreSession(), request.getTitle(),
176                    request.getQueryParams(), null, request.getQuery(), request.getQueryLanguage(),
177                    request.getPageProviderName(), request.getPageSize(), request.getCurrentPageIndex(),
178                    request.getMaxResults(), request.getSortBy(), request.getSortOrder(), request.getContentViewData());
179            setSaveSearchParams(request.getNamedParams(), search);
180            return Response.ok(savedSearchService.saveSavedSearch(ctx.getCoreSession(), search)).build();
181        } catch (InvalidSearchParameterException | IOException e) {
182            throw new NuxeoException(e.getMessage(), SC_BAD_REQUEST);
183        }
184    }
185
186    @GET
187    @Path("saved/{id}")
188    public Response doGetSavedSearch(@PathParam("id") String id) {
189        SavedSearch search = savedSearchService.getSavedSearch(ctx.getCoreSession(), id);
190        if (search == null) {
191            return Response.status(Response.Status.NOT_FOUND).build();
192        }
193        return Response.ok(search).build();
194    }
195
196    @Path("saved/{id}/bulk")
197    public Object doBulkActionBySavedSearch(@PathParam("id") String id, @Context UriInfo uriInfo) {
198        SavedSearch search = savedSearchService.getSavedSearch(ctx.getCoreSession(), id);
199        if (search == null) {
200            return Response.status(Response.Status.NOT_FOUND).build();
201        }
202        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
203        String query = getQueryString(search.getPageProviderName(), queryParams);
204        return newObject("bulkAction", query);
205    }
206
207    @PUT
208    @Path("saved/{id}")
209    @Consumes(MediaType.APPLICATION_JSON)
210    public Response doUpdateSavedSearch(SavedSearchRequest request, @PathParam("id") String id) {
211        SavedSearch search = savedSearchService.getSavedSearch(ctx.getCoreSession(), id);
212        if (search == null) {
213            return Response.status(Response.Status.NOT_FOUND).build();
214        }
215
216        search.setTitle(request.getTitle());
217        search.setQueryParams(request.getQueryParams());
218        search.setQuery(request.getQuery());
219        search.setQueryLanguage(request.getQueryLanguage());
220        search.setPageProviderName(request.getPageProviderName());
221        search.setPageSize(request.getPageSize());
222        search.setCurrentPageIndex(request.getCurrentPageIndex());
223        search.setMaxResults(request.getMaxResults());
224        search.setSortBy(request.getSortBy());
225        search.setSortOrder(request.getSortOrder());
226        search.setContentViewData(request.getContentViewData());
227        try {
228            setSaveSearchParams(request.getNamedParams(), search);
229            search = savedSearchService.saveSavedSearch(ctx.getCoreSession(), search);
230        } catch (InvalidSearchParameterException | IOException e) {
231            throw new NuxeoException(e.getMessage(), SC_BAD_REQUEST);
232        }
233        return Response.ok(search).build();
234    }
235
236    @DELETE
237    @Path("saved/{id}")
238    public Response doDeleteSavedSearch(@PathParam("id") String id) {
239        SavedSearch search = savedSearchService.getSavedSearch(ctx.getCoreSession(), id);
240        savedSearchService.deleteSavedSearch(ctx.getCoreSession(), search);
241        return Response.status(Response.Status.NO_CONTENT).build();
242    }
243
244    @GET
245    @Path("saved/{id}/execute")
246    public Object doExecuteSavedSearch(@PathParam("id") String id, @Context UriInfo uriInfo) {
247        MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
248        SavedSearch search = savedSearchService.getSavedSearch(ctx.getCoreSession(), id);
249        if (search == null) {
250            return Response.status(Response.Status.NOT_FOUND).build();
251        }
252        return executeSavedSearch(search, params);
253    }
254
255    protected void setSaveSearchParams(Map<String, String> params, SavedSearch search) throws IOException {
256        Map<String, String> namedParams = new HashMap<>();
257        if (params != null) {
258            for (Map.Entry<String, String> entry : params.entrySet()) {
259                String key = entry.getKey();
260                String value = entry.getValue();
261                try {
262                    Property prop = search.getDocument().getProperty(key);
263                    DocumentHelper.setProperty(search.getDocument().getCoreSession(), search.getDocument(), key, value,
264                            true);
265                } catch (PropertyNotFoundException e) {
266                    namedParams.put(key, value);
267                }
268            }
269        }
270        search.setNamedParams(namedParams);
271    }
272
273    protected DocumentModelList executeSavedSearch(SavedSearch search, MultivaluedMap<String, String> params) {
274        Long pageSize = getPageSize(params);
275        Long currentPageIndex = getCurrentPageIndex(params);
276        Long currentPageOffset = getCurrentPageOffset(params);
277        Long maxResults = getMaxResults(params);
278        List<SortInfo> sortInfo = getSortInfo(params);
279
280        if (!StringUtils.isEmpty(search.getPageProviderName())) {
281            List<QuickFilter> quickFilters = getQuickFilters(search.getPageProviderName(), params);
282            return querySavedSearchByPageProvider(search.getPageProviderName(),
283                    pageSize != null ? pageSize : search.getPageSize(),
284                    currentPageIndex != null ? currentPageIndex : search.getCurrentPageIndex(),
285                    currentPageOffset != null ? currentPageOffset : search.getCurrentPageOffset(),
286                    search.getQueryParams(), search.getNamedParams(),
287                    sortInfo != null ? sortInfo : getSortInfo(search.getSortBy(), search.getSortOrder()), quickFilters,
288                    !search.getDocument().getType().equals(SavedSearchConstants.PARAMETERIZED_SAVED_SEARCH_TYPE_NAME)
289                            ? search.getDocument()
290                            : null);
291        } else if (!StringUtils.isEmpty(search.getQuery()) && !StringUtils.isEmpty(search.getQueryLanguage())) {
292            return querySavedSearchByLang(search.getQueryLanguage(), search.getQuery(),
293                    pageSize != null ? pageSize : search.getPageSize(),
294                    currentPageIndex != null ? currentPageIndex : search.getCurrentPageIndex(),
295                    currentPageOffset != null ? currentPageOffset : search.getCurrentPageOffset(),
296                    maxResults != null ? maxResults : search.getMaxResults(), search.getQueryParams(),
297                    search.getNamedParams(),
298                    sortInfo != null ? sortInfo : getSortInfo(search.getSortBy(), search.getSortOrder()));
299        } else {
300            return null;
301        }
302    }
303
304    protected DocumentModelList querySavedSearchByLang(String queryLanguage, String query, Long pageSize,
305            Long currentPageIndex, Long currentPageOffset, Long maxResults, String orderedParams,
306            Map<String, String> namedParameters, List<SortInfo> sortInfo) {
307        Map<String, String> namedParametersProps = getNamedParameters(namedParameters);
308        Object[] parameters = replaceParameterPattern(new Object[] { orderedParams });
309        Map<String, Serializable> props = getProperties();
310
311        DocumentModel searchDocumentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(), null,
312                namedParametersProps);
313
314        return queryByLang(query, pageSize, currentPageIndex, currentPageOffset, maxResults, sortInfo,
315                props, searchDocumentModel, parameters);
316    }
317
318    protected DocumentModelList querySavedSearchByPageProvider(String pageProviderName, Long pageSize,
319            Long currentPageIndex, Long currentPageOffset, String orderedParams, Map<String, String> namedParameters,
320            List<SortInfo> sortInfo, List<QuickFilter> quickFilters, DocumentModel searchDocumentModel) {
321        Map<String, String> namedParametersProps = getNamedParameters(namedParameters);
322        Object[] parameters = orderedParams != null ? replaceParameterPattern(new Object[] { orderedParams })
323                : new Object[0];
324        Map<String, Serializable> props = getProperties();
325
326        DocumentModel documentModel;
327        if (searchDocumentModel == null) {
328            documentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(), pageProviderName,
329                    namedParametersProps);
330        } else {
331            documentModel = searchDocumentModel;
332            if (namedParametersProps.size() > 0) {
333                documentModel.putContextData(PageProviderService.NAMED_PARAMETERS, (Serializable) namedParametersProps);
334            }
335        }
336
337        return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo,
338                quickFilters, parameters, props, documentModel);
339    }
340
341    /**
342     * Retrieves the query string from the page provider and/or the query parameters.
343     * 
344     * @param providerName the page provider name
345     * @param parameters the parameters
346     * @return the query string
347     */
348    protected String getQueryString(String providerName, MultivaluedMap<String, String> parameters) {
349
350        Map<String, String> namedParameters = getNamedParameters(parameters);
351        Object[] queryParameters = getParameters(parameters);
352        List<String> quickfilters = asStringList(parameters.getFirst(QUICK_FILTERS));
353        Long pageSize = getPageSize(parameters);
354        Long currentPageIndex = getCurrentPageIndex(parameters);
355        List<String> sortBy = asStringList(parameters.getFirst(SORT_BY));
356        List<String> sortOrder = asStringList(parameters.getFirst(SORT_ORDER));
357
358        String query = getQuery(parameters);
359
360        PageProviderDefinition def = providerName == null ? PageProviderHelper.getQueryPageProviderDefinition(query)
361                : PageProviderHelper.getPageProviderDefinition(providerName);
362
363        PageProvider provider = PageProviderHelper.getPageProvider(ctx.getCoreSession(), def, namedParameters, sortBy, sortOrder, pageSize, currentPageIndex, null, quickfilters, queryParameters);
364        return PageProviderHelper.buildQueryString(provider);
365    }
366}