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.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.ws.rs.Consumes;
031import javax.ws.rs.DELETE;
032import javax.ws.rs.GET;
033import javax.ws.rs.POST;
034import javax.ws.rs.PUT;
035import javax.ws.rs.Path;
036import javax.ws.rs.PathParam;
037import javax.ws.rs.core.Context;
038import javax.ws.rs.core.MediaType;
039import javax.ws.rs.core.MultivaluedMap;
040import javax.ws.rs.core.Response;
041import javax.ws.rs.core.UriInfo;
042
043import org.apache.commons.lang3.EnumUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.nuxeo.ecm.automation.core.util.DocumentHelper;
046import org.nuxeo.ecm.automation.core.util.PageProviderHelper;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.DocumentModelList;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.ecm.core.api.SortInfo;
051import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
052import org.nuxeo.ecm.platform.query.api.PageProvider;
053import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
054import org.nuxeo.ecm.platform.query.api.PageProviderService;
055import org.nuxeo.ecm.platform.query.api.QuickFilter;
056import org.nuxeo.ecm.platform.search.core.InvalidSearchParameterException;
057import org.nuxeo.ecm.platform.search.core.SavedSearch;
058import org.nuxeo.ecm.platform.search.core.SavedSearchConstants;
059import org.nuxeo.ecm.platform.search.core.SavedSearchRequest;
060import org.nuxeo.ecm.platform.search.core.SavedSearchService;
061import org.nuxeo.ecm.webengine.model.WebObject;
062import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException;
063import org.nuxeo.runtime.api.Framework;
064
065/**
066 * @since 8.3 Search endpoint to perform queries via rest api, with support to save and execute saved search queries.
067 */
068@WebObject(type = "search")
069public class SearchObject extends QueryExecutor {
070
071    public static final String SAVED_SEARCHES_PAGE_PROVIDER = "SAVED_SEARCHES_ALL";
072
073    public static final String SAVED_SEARCHES_PAGE_PROVIDER_PARAMS = "SAVED_SEARCHES_ALL_PAGE_PROVIDER";
074
075    public static final String PAGE_PROVIDER_NAME_PARAM = "pageProvider";
076
077    protected SavedSearchService savedSearchService;
078
079    @Override
080    public void initialize(Object... args) {
081        initExecutor();
082        savedSearchService = Framework.getService(SavedSearchService.class);
083    }
084
085    /**
086     * @deprecated since 10.3, use {@link #doQueryByLang(UriInfo)} instead.
087     */
088    @GET
089    @Path("lang/{queryLanguage}/execute")
090    @Deprecated
091    public Object doQueryByLang(@Context UriInfo uriInfo, @PathParam("queryLanguage") String queryLanguage) {
092        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
093        return queryByLang(queryLanguage, queryParams);
094    }
095
096    /**
097     * @since 10.3
098     */
099    @GET
100    @Path("execute")
101    public Object doQueryByLang(@Context UriInfo uriInfo) {
102        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
103        return queryByLang(queryParams);
104    }
105
106    /**
107     * @deprecated since 10.3, use {@link #doBulkActionByLang(UriInfo)} instead.
108     */
109    @Path("lang/{queryLanguage}/bulk")
110    @Deprecated
111    public Object doBulkActionByLang(@Context UriInfo uriInfo, @PathParam("queryLanguage") String queryLanguage) {
112        if (!EnumUtils.isValidEnum(LangParams.class, queryLanguage)) {
113            throw new IllegalParameterException("invalid query language");
114        }
115        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
116        String query = getQueryString(null, queryParams);
117        return newObject("bulkAction", query);
118    }
119
120    /**
121     * @since 10.3
122     */
123    @Path("bulk")
124    public Object doBulkActionByLang(@Context UriInfo uriInfo) {
125        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
126        String query = getQueryString(null, queryParams);
127        String scrollName = queryParams.getFirst(SCROLL_PARAM);
128        return newObject("bulkAction", query, scrollName);
129    }
130
131    @GET
132    @Path("pp/{pageProviderName}/execute")
133    public Object doQueryByPageProvider(@Context UriInfo uriInfo,
134            @PathParam("pageProviderName") String pageProviderName) {
135        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
136        return queryByPageProvider(pageProviderName, queryParams);
137    }
138
139    @GET
140    @Path("pp/{pageProviderName}")
141    public Object doGetPageProviderDefinition(@PathParam("pageProviderName") String pageProviderName)
142            throws IOException {
143        return buildResponse(Response.Status.OK, MediaType.APPLICATION_JSON,
144                getPageProviderDefinition(pageProviderName));
145    }
146
147    @Path("pp/{pageProviderName}/bulk")
148    public Object doBulkActionByPageProvider(@PathParam("pageProviderName") String pageProviderName,
149            @Context UriInfo uriInfo) {
150        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
151        String query = getQueryString(pageProviderName, queryParams);
152        String scrollName = queryParams.getFirst(SCROLL_PARAM);
153        return newObject("bulkAction", query, scrollName);
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                    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}