/* * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Gabriel Barata <gbarata@nuxeo.com> */ package org.nuxeo.ecm.restapi.server.jaxrs.search; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.map.ObjectMapper; import org.nuxeo.ecm.automation.core.util.DocumentHelper; import org.nuxeo.ecm.automation.core.util.Properties; import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl; import org.nuxeo.ecm.automation.server.jaxrs.RestOperationException; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.SortInfo; import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.ecm.platform.query.api.PageProvider; import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; import org.nuxeo.ecm.platform.query.api.PageProviderService; import org.nuxeo.ecm.platform.query.api.QuickFilter; import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider; import org.nuxeo.ecm.restapi.server.jaxrs.adapters.SearchAdapter; import org.nuxeo.ecm.webengine.model.impl.AbstractResource; import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl; import org.nuxeo.runtime.api.Framework; /** * @since 8.3 */ public abstract class QueryExecutor extends AbstractResource<ResourceTypeImpl> { public static final String NXQL = "NXQL"; public static final String QUERY = "query"; public static final String PAGE_SIZE = "pageSize"; public static final String CURRENT_PAGE_INDEX = "currentPageIndex"; public static final String MAX_RESULTS = "maxResults"; public static final String SORT_BY = "sortBy"; public static final String SORT_ORDER = "sortOrder"; public static final String ORDERED_PARAMS = "queryParams"; /** * @since 8.4 */ public static final String QUICK_FILTERS = "quickFilters"; /** * @since 9.1 */ public static final String HIGHLIGHT = "highlight"; public static final String CURRENT_USERID_PATTERN = "$currentUser"; public static final String CURRENT_REPO_PATTERN = "$currentRepository"; public enum QueryParams { PAGE_SIZE, CURRENT_PAGE_INDEX, MAX_RESULTS, SORT_BY, SORT_ORDER, ORDERED_PARAMS, QUERY } public enum LangParams { NXQL } protected PageProviderService pageProviderService; private static final Log log = LogFactory.getLog(SearchObject.class); public void initExecutor() { pageProviderService = Framework.getService(PageProviderService.class); } protected String getQuery(MultivaluedMap<String, String> queryParams) { String query = queryParams.getFirst(QUERY); if (query == null) { query = "SELECT * FROM Document"; } return query; } protected Long getCurrentPageIndex(MultivaluedMap<String, String> queryParams) { String currentPageIndex = queryParams.getFirst(CURRENT_PAGE_INDEX); if (currentPageIndex != null && !currentPageIndex.isEmpty()) { return Long.valueOf(currentPageIndex); } return null; } protected Long getPageSize(MultivaluedMap<String, String> queryParams) { String pageSize = queryParams.getFirst(PAGE_SIZE); if (pageSize != null && !pageSize.isEmpty()) { return Long.valueOf(pageSize); } return null; } protected Long getMaxResults(MultivaluedMap<String, String> queryParams) { String maxResults = queryParams.getFirst(MAX_RESULTS); if (maxResults != null && !maxResults.isEmpty()) { return Long.valueOf(maxResults); } return null; } protected List<SortInfo> getSortInfo(MultivaluedMap<String, String> queryParams) { String sortBy = queryParams.getFirst(SORT_BY); String sortOrder = queryParams.getFirst(SORT_ORDER); List<SortInfo> sortInfoList = new ArrayList<>(); if (!StringUtils.isBlank(sortBy)) { String[] sorts = sortBy.split(","); String[] orders = null; if (!StringUtils.isBlank(sortOrder)) { orders = sortOrder.split(","); } for (int i = 0; i < sorts.length; i++) { String sort = sorts[i]; boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase())); sortInfoList.add(new SortInfo(sort, sortAscending)); } } return sortInfoList; } protected List<SortInfo> getSortInfo(String sortBy, String sortOrder) { List<SortInfo> sortInfoList = new ArrayList<>(); if (!StringUtils.isBlank(sortBy)) { String[] sorts = sortBy.split(","); String[] orders = null; if (!StringUtils.isBlank(sortOrder)) { orders = sortOrder.split(","); } for (int i = 0; i < sorts.length; i++) { String sort = sorts[i]; boolean sortAscending = (orders != null && orders.length > i && "asc".equals(orders[i].toLowerCase())); sortInfoList.add(new SortInfo(sort, sortAscending)); } } return sortInfoList; } /** * @since 8.4 */ protected List<QuickFilter> getQuickFilters(String providerName, MultivaluedMap<String, String> queryParams) { PageProviderDefinition pageProviderDefinition = pageProviderService.getPageProviderDefinition(providerName); String quickFilters = queryParams.getFirst(QUICK_FILTERS); List<QuickFilter> quickFilterList = new ArrayList<>(); if (!StringUtils.isBlank(quickFilters)) { String[] filters = quickFilters.split(","); List<QuickFilter> ppQuickFilters = pageProviderDefinition.getQuickFilters(); for (String filter : filters) { for (QuickFilter quickFilter : ppQuickFilters) { if (quickFilter.getName().equals(filter)) { quickFilterList.add(quickFilter); break; } } } } return quickFilterList; } protected List<String> getHighlights(MultivaluedMap<String, String> queryParams) { String highlight = queryParams.getFirst(HIGHLIGHT); List<String> highlightFields = new ArrayList<>(); if (!StringUtils.isBlank(highlight)) { String[] fields = highlight.split(","); highlightFields = Arrays.asList(fields); } return highlightFields; } protected Properties getNamedParameters(MultivaluedMap<String, String> queryParams) { Properties namedParameters = new Properties(); for (String namedParameterKey : queryParams.keySet()) { if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) { String value = queryParams.getFirst(namedParameterKey); namedParameters.put(namedParameterKey, handleNamedParamVars(value)); } } return namedParameters; } protected Properties getNamedParameters(Map<String, String> queryParams) { Properties namedParameters = new Properties(); for (String namedParameterKey : queryParams.keySet()) { if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) { String value = queryParams.get(namedParameterKey); namedParameters.put(namedParameterKey, handleNamedParamVars(value)); } } return namedParameters; } protected String handleNamedParamVars(String value) { if (value != null) { if (value.equals(CURRENT_USERID_PATTERN)) { return ctx.getCoreSession().getPrincipal().getName(); } else if (value.equals(CURRENT_REPO_PATTERN)) { return ctx.getCoreSession().getRepositoryName(); } } return value; } protected Object[] getParameters(MultivaluedMap<String, String> queryParams) { List<String> orderedParams = queryParams.get(ORDERED_PARAMS); if (orderedParams != null && !orderedParams.isEmpty()) { Object[] parameters = orderedParams.toArray(new String[orderedParams.size()]); // expand specific parameters replaceParameterPattern(parameters); return parameters; } return null; } protected Object[] replaceParameterPattern(Object[] parameters) { for (int idx = 0; idx < parameters.length; idx++) { String value = (String) parameters[idx]; if (value.equals(CURRENT_USERID_PATTERN)) { parameters[idx] = ctx.getCoreSession().getPrincipal().getName(); } else if (value.equals(CURRENT_REPO_PATTERN)) { parameters[idx] = ctx.getCoreSession().getRepositoryName(); } } return parameters; } protected Map<String, Serializable> getProperties() { Map<String, Serializable> props = new HashMap<String, Serializable>(); props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) ctx.getCoreSession()); return props; } protected DocumentModelList queryByLang(String queryLanguage, MultivaluedMap<String, String> queryParams) throws RestOperationException { if (queryLanguage == null || !EnumUtils.isValidEnum(LangParams.class, queryLanguage)) { throw new RestOperationException("invalid query language", HttpServletResponse.SC_BAD_REQUEST); } String query = getQuery(queryParams); Long pageSize = getPageSize(queryParams); Long currentPageIndex = getCurrentPageIndex(queryParams); Long maxResults = getMaxResults(queryParams); Properties namedParameters = getNamedParameters(queryParams); Object[] parameters = getParameters(queryParams); List<SortInfo> sortInfo = getSortInfo(queryParams); Map<String, Serializable> props = getProperties(); DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService, null, namedParameters); return queryByLang(query, pageSize, currentPageIndex, maxResults, sortInfo, parameters, props, searchDocumentModel); } protected DocumentModelList queryByPageProvider(String pageProviderName, MultivaluedMap<String, String> queryParams) throws RestOperationException { if (pageProviderName == null) { throw new RestOperationException("invalid page provider name", HttpServletResponse.SC_BAD_REQUEST); } Long pageSize = getPageSize(queryParams); Long currentPageIndex = getCurrentPageIndex(queryParams); Properties namedParameters = getNamedParameters(queryParams); Object[] parameters = getParameters(queryParams); List<SortInfo> sortInfo = getSortInfo(queryParams); List<QuickFilter> quickFilters = getQuickFilters(pageProviderName, queryParams); List<String> highlights = getHighlights(queryParams); Map<String, Serializable> props = getProperties(); DocumentModel searchDocumentModel = getSearchDocumentModel(ctx.getCoreSession(), pageProviderService, pageProviderName, namedParameters); return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, sortInfo, highlights, quickFilters, parameters, props, searchDocumentModel); } protected DocumentModelList queryByLang(String query, Long pageSize, Long currentPageIndex, Long maxResults, List<SortInfo> sortInfo, Object[] parameters, Map<String, Serializable> props, DocumentModel searchDocumentModel) throws RestOperationException { PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition( SearchAdapter.pageProviderName); ppdefinition.setPattern(query); if (maxResults != null && maxResults != -1) { // set the maxResults to avoid slowing down queries ppdefinition.getProperties().put("maxResults", maxResults.toString()); } PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl( (PageProvider<DocumentModel>) pageProviderService.getPageProvider(SearchAdapter.pageProviderName, ppdefinition, searchDocumentModel, sortInfo, pageSize, currentPageIndex, props, parameters), null); if (res.hasError()) { RestOperationException err = new RestOperationException(res.getErrorMessage()); err.setStatus(HttpServletResponse.SC_BAD_REQUEST); throw err; } return res; } /** * @since 8.4 */ protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex, List<SortInfo> sortInfo, List<QuickFilter> quickFilters, Object[] parameters, Map<String, Serializable> props, DocumentModel searchDocumentModel) throws RestOperationException { return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, sortInfo, null, quickFilters, parameters, props, searchDocumentModel); } protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex, List<SortInfo> sortInfo, List<String> highlights, List<QuickFilter> quickFilters, Object[] parameters, Map<String, Serializable> props, DocumentModel searchDocumentModel) throws RestOperationException { PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl( (PageProvider<DocumentModel>) pageProviderService.getPageProvider(pageProviderName, searchDocumentModel, sortInfo, pageSize, currentPageIndex, props, highlights, quickFilters, parameters), null); if (res.hasError()) { RestOperationException err = new RestOperationException(res.getErrorMessage()); err.setStatus(HttpServletResponse.SC_BAD_REQUEST); throw err; } return res; } protected PageProviderDefinition getPageProviderDefinition(String pageProviderName) throws IOException { return pageProviderService.getPageProviderDefinition(pageProviderName); } protected DocumentModel getSearchDocumentModel(CoreSession session, PageProviderService pps, String providerName, Properties namedParameters) { // generate search document model if type specified on the definition DocumentModel searchDocumentModel = null; if (!StringUtils.isBlank(providerName)) { PageProviderDefinition pageProviderDefinition = pps.getPageProviderDefinition(providerName); if (pageProviderDefinition != null) { String searchDocType = pageProviderDefinition.getSearchDocumentType(); if (searchDocType != null) { searchDocumentModel = session.createDocumentModel(searchDocType); } else if (pageProviderDefinition.getWhereClause() != null) { // avoid later error on null search doc, in case where clause is only referring to named parameters // (and no namedParameters are given) searchDocumentModel = new SimpleDocumentModel(); } } else { log.error("No page provider definition found for " + providerName); } } if (namedParameters != null && !namedParameters.isEmpty()) { // fall back on simple document if no type defined on page provider if (searchDocumentModel == null) { searchDocumentModel = new SimpleDocumentModel(); } for (Map.Entry<String, String> entry : namedParameters.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); try { DocumentHelper.setProperty(session, searchDocumentModel, key, value, true); } catch (PropertyNotFoundException | IOException e) { // assume this is a "pure" named parameter, not part of the search doc schema continue; } } searchDocumentModel.putContextData(PageProviderService.NAMED_PARAMETERS, namedParameters); } return searchDocumentModel; } protected Response buildResponse(Response.StatusType status, String type, Object object) throws IOException { ObjectMapper mapper = new ObjectMapper(); String message = mapper.writeValueAsString(object); return Response.status(status) .header("Content-Length", message.getBytes("UTF-8").length) .type(type + "; charset=UTF-8") .entity(message) .build(); } }