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.Arrays; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030 031import javax.ws.rs.core.MultivaluedMap; 032import javax.ws.rs.core.Response; 033 034import org.apache.commons.lang3.EnumUtils; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.automation.core.util.PageProviderHelper; 039import org.nuxeo.ecm.automation.jaxrs.io.documents.PaginableDocumentModelListImpl; 040import org.nuxeo.ecm.core.api.DocumentModel; 041import org.nuxeo.ecm.core.api.DocumentModelList; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.SortInfo; 044import org.nuxeo.ecm.platform.query.api.PageProvider; 045import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 046import org.nuxeo.ecm.platform.query.api.PageProviderService; 047import org.nuxeo.ecm.platform.query.api.QuickFilter; 048import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider; 049import org.nuxeo.ecm.restapi.server.jaxrs.adapters.SearchAdapter; 050import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException; 051import org.nuxeo.ecm.webengine.model.impl.AbstractResource; 052import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl; 053import org.nuxeo.runtime.api.Framework; 054 055import com.fasterxml.jackson.databind.ObjectMapper; 056 057/** 058 * @since 8.3 059 */ 060public abstract class QueryExecutor extends AbstractResource<ResourceTypeImpl> { 061 062 public static final String NXQL = "NXQL"; 063 064 public static final String QUERY = "query"; 065 066 public static final String PAGE_SIZE = "pageSize"; 067 068 public static final String CURRENT_PAGE_INDEX = "currentPageIndex"; 069 070 /** 071 * In case offset is specified, currentPageIndex is ignored. 072 * 073 * @since 9.3 074 */ 075 public static final String CURRENT_PAGE_OFFSET = "offset"; 076 077 public static final String MAX_RESULTS = "maxResults"; 078 079 public static final String SORT_BY = "sortBy"; 080 081 public static final String SORT_ORDER = "sortOrder"; 082 083 public static final String ORDERED_PARAMS = "queryParams"; 084 085 /** 086 * @since 8.4 087 */ 088 public static final String QUICK_FILTERS = "quickFilters"; 089 090 /** 091 * @since 9.1 092 */ 093 public static final String HIGHLIGHT = "highlight"; 094 095 public static final String CURRENT_USERID_PATTERN = "$currentUser"; 096 097 public static final String CURRENT_REPO_PATTERN = "$currentRepository"; 098 099 public enum QueryParams { 100 PAGE_SIZE, CURRENT_PAGE_INDEX, MAX_RESULTS, SORT_BY, SORT_ORDER, ORDERED_PARAMS, QUERY 101 } 102 103 public enum LangParams { 104 NXQL 105 } 106 107 // @since 11.1 108 public static final String SCROLL_PARAM = "scroll"; 109 110 protected PageProviderService pageProviderService; 111 112 protected boolean skipAggregates; 113 114 private static final Log log = LogFactory.getLog(SearchObject.class); 115 116 public void initExecutor() { 117 pageProviderService = Framework.getService(PageProviderService.class); 118 skipAggregates = Boolean.parseBoolean( 119 ctx.getHttpHeaders().getRequestHeaders().getFirst(PageProvider.SKIP_AGGREGATES_PROP)); 120 } 121 122 protected String getQuery(MultivaluedMap<String, String> queryParams) { 123 String query = queryParams.getFirst(QUERY); 124 if (query == null) { 125 query = "SELECT * FROM Document"; 126 } 127 return query; 128 } 129 130 protected Long getCurrentPageIndex(MultivaluedMap<String, String> queryParams) { 131 String currentPageIndex = queryParams.getFirst(CURRENT_PAGE_INDEX); 132 if (currentPageIndex != null && !currentPageIndex.isEmpty()) { 133 return Long.valueOf(currentPageIndex); 134 } 135 return null; 136 } 137 138 protected Long getCurrentPageOffset(MultivaluedMap<String, String> queryParams) { 139 String currentPageOffset = queryParams.getFirst(CURRENT_PAGE_OFFSET); 140 if (currentPageOffset != null && !currentPageOffset.isEmpty()) { 141 return Long.valueOf(currentPageOffset); 142 } 143 return null; 144 } 145 146 protected Long getPageSize(MultivaluedMap<String, String> queryParams) { 147 String pageSize = queryParams.getFirst(PAGE_SIZE); 148 if (pageSize != null && !pageSize.isEmpty()) { 149 return Long.valueOf(pageSize); 150 } 151 return null; 152 } 153 154 protected Long getMaxResults(MultivaluedMap<String, String> queryParams) { 155 String maxResults = queryParams.getFirst(MAX_RESULTS); 156 if (maxResults != null && !maxResults.isEmpty()) { 157 return Long.valueOf(maxResults); 158 } 159 return null; 160 } 161 162 protected List<SortInfo> getSortInfo(MultivaluedMap<String, String> queryParams) { 163 String sortBy = queryParams.getFirst(SORT_BY); 164 String sortOrder = queryParams.getFirst(SORT_ORDER); 165 return getSortInfo(sortBy, sortOrder); 166 } 167 168 protected List<SortInfo> getSortInfo(String sortBy, String sortOrder) { 169 List<SortInfo> sortInfoList = null; 170 if (!StringUtils.isBlank(sortBy)) { 171 String[] sorts = sortBy.split(","); 172 String[] orders = null; 173 if (!StringUtils.isBlank(sortOrder)) { 174 orders = sortOrder.split(","); 175 } 176 if (sorts.length > 0) { 177 sortInfoList = new ArrayList<>(); 178 } 179 for (int i = 0; i < sorts.length; i++) { 180 String sort = sorts[i]; 181 boolean sortAscending = orders != null && orders.length > i && "asc".equalsIgnoreCase(orders[i]); 182 sortInfoList.add(new SortInfo(sort, sortAscending)); // NOSONAR 183 } 184 } 185 return sortInfoList; 186 } 187 188 /** 189 * @since 8.4 190 */ 191 protected List<QuickFilter> getQuickFilters(String providerName, MultivaluedMap<String, String> queryParams) { 192 PageProviderDefinition pageProviderDefinition = pageProviderService.getPageProviderDefinition(providerName); 193 String quickFilters = queryParams.getFirst(QUICK_FILTERS); 194 List<QuickFilter> quickFilterList = new ArrayList<>(); 195 if (!StringUtils.isBlank(quickFilters)) { 196 String[] filters = quickFilters.split(","); 197 List<QuickFilter> ppQuickFilters = pageProviderDefinition.getQuickFilters(); 198 for (String filter : filters) { 199 for (QuickFilter quickFilter : ppQuickFilters) { 200 if (quickFilter.getName().equals(filter)) { 201 quickFilterList.add(quickFilter); 202 break; 203 } 204 } 205 } 206 } 207 return quickFilterList; 208 } 209 210 protected List<String> getHighlights(MultivaluedMap<String, String> queryParams) { 211 String highlight = queryParams.getFirst(HIGHLIGHT); 212 List<String> highlightFields = new ArrayList<>(); 213 if (!StringUtils.isBlank(highlight)) { 214 String[] fields = highlight.split(","); 215 highlightFields = Arrays.asList(fields); 216 } 217 return highlightFields; 218 } 219 220 protected Map<String, String> getNamedParameters(MultivaluedMap<String, String> queryParams) { 221 Map<String, String> namedParameters = new HashMap<>(); 222 for (String namedParameterKey : queryParams.keySet()) { 223 if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) { 224 String value = queryParams.getFirst(namedParameterKey); 225 namedParameters.put(namedParameterKey, handleNamedParamVars(value)); 226 } 227 } 228 return namedParameters; 229 } 230 231 protected Map<String, String> getNamedParameters(Map<String, String> queryParams) { 232 Map<String, String> namedParameters = new HashMap<>(); 233 for (String namedParameterKey : queryParams.keySet()) { 234 if (!EnumUtils.isValidEnum(QueryParams.class, namedParameterKey)) { 235 String value = queryParams.get(namedParameterKey); 236 namedParameters.put(namedParameterKey, handleNamedParamVars(value)); 237 } 238 } 239 return namedParameters; 240 } 241 242 protected String handleNamedParamVars(String value) { 243 if (value != null) { 244 if (value.equals(CURRENT_USERID_PATTERN)) { 245 return ctx.getCoreSession().getPrincipal().getName(); 246 } else if (value.equals(CURRENT_REPO_PATTERN)) { 247 return ctx.getCoreSession().getRepositoryName(); 248 } 249 } 250 return value; 251 } 252 253 protected Object[] getParameters(MultivaluedMap<String, String> queryParams) { 254 List<String> orderedParams = queryParams.get(ORDERED_PARAMS); 255 if (orderedParams != null && !orderedParams.isEmpty()) { 256 Object[] parameters = orderedParams.toArray(new String[orderedParams.size()]); 257 // expand specific parameters 258 replaceParameterPattern(parameters); 259 return parameters; 260 } 261 return null; 262 } 263 264 protected Object[] replaceParameterPattern(Object[] parameters) { 265 for (int idx = 0; idx < parameters.length; idx++) { 266 String value = (String) parameters[idx]; 267 if (value.equals(CURRENT_USERID_PATTERN)) { 268 parameters[idx] = ctx.getCoreSession().getPrincipal().getName(); 269 } else if (value.equals(CURRENT_REPO_PATTERN)) { 270 parameters[idx] = ctx.getCoreSession().getRepositoryName(); 271 } 272 } 273 return parameters; 274 } 275 276 protected Map<String, Serializable> getProperties() { 277 Map<String, Serializable> props = new HashMap<>(); 278 props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) ctx.getCoreSession()); 279 props.put(PageProvider.SKIP_AGGREGATES_PROP, skipAggregates); 280 return props; 281 } 282 283 protected DocumentModelList queryByLang(String queryLanguage, MultivaluedMap<String, String> queryParams) { 284 if (queryLanguage == null || !EnumUtils.isValidEnum(LangParams.class, queryLanguage)) { 285 throw new IllegalParameterException("invalid query language"); 286 } 287 return queryByLang(queryParams); 288 } 289 290 protected DocumentModelList queryByLang(MultivaluedMap<String, String> queryParams) { 291 String query = getQuery(queryParams); 292 Long pageSize = getPageSize(queryParams); 293 Long currentPageIndex = getCurrentPageIndex(queryParams); 294 Long currentPageOffset = getCurrentPageOffset(queryParams); 295 Long maxResults = getMaxResults(queryParams); 296 Map<String, String> namedParameters = getNamedParameters(queryParams); 297 Object[] parameters = getParameters(queryParams); 298 List<SortInfo> sortInfo = getSortInfo(queryParams); 299 Map<String, Serializable> props = getProperties(); 300 301 DocumentModel searchDocumentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(), null, 302 namedParameters); 303 304 return queryByLang(query, pageSize, currentPageIndex, currentPageOffset, maxResults, sortInfo, 305 props, searchDocumentModel, parameters); 306 } 307 308 protected DocumentModelList queryByPageProvider(String pageProviderName, 309 MultivaluedMap<String, String> queryParams) { 310 if (pageProviderName == null) { 311 throw new IllegalParameterException("invalid page provider name"); 312 } 313 314 Long pageSize = getPageSize(queryParams); 315 Long currentPageIndex = getCurrentPageIndex(queryParams); 316 Long currentPageOffset = getCurrentPageOffset(queryParams); 317 Map<String, String> namedParameters = getNamedParameters(queryParams); 318 Object[] parameters = getParameters(queryParams); 319 List<SortInfo> sortInfo = getSortInfo(queryParams); 320 List<QuickFilter> quickFilters = getQuickFilters(pageProviderName, queryParams); 321 List<String> highlights = getHighlights(queryParams); 322 Map<String, Serializable> props = getProperties(); 323 324 DocumentModel searchDocumentModel = PageProviderHelper.getSearchDocumentModel(ctx.getCoreSession(), 325 pageProviderName, namedParameters); 326 327 return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo, 328 highlights, quickFilters, props, searchDocumentModel, parameters); 329 } 330 331 @SuppressWarnings("unchecked") 332 protected DocumentModelList queryByLang(String query, Long pageSize, Long currentPageIndex, Long currentPageOffset, 333 Long maxResults, List<SortInfo> sortInfo, Map<String, Serializable> props, 334 DocumentModel searchDocumentModel, Object... parameters) { 335 PageProviderDefinition ppdefinition = pageProviderService.getPageProviderDefinition( 336 SearchAdapter.pageProviderName); 337 ppdefinition.setPattern(query); 338 if (maxResults != null && maxResults != -1) { 339 // set the maxResults to avoid slowing down queries 340 ppdefinition.getProperties().put("maxResults", maxResults.toString()); 341 } 342 PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl( 343 (PageProvider<DocumentModel>) pageProviderService.getPageProvider(SearchAdapter.pageProviderName, 344 ppdefinition, searchDocumentModel, sortInfo, pageSize, currentPageIndex, currentPageOffset, 345 props, null, null, parameters), 346 null); 347 if (res.hasError()) { 348 throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST); 349 } 350 return res; 351 } 352 353 /** 354 * @since 8.4 355 */ 356 protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex, 357 Long currentPageOffset, List<SortInfo> sortInfo, List<QuickFilter> quickFilters, Object[] parameters, 358 Map<String, Serializable> props, DocumentModel searchDocumentModel) { 359 return queryByPageProvider(pageProviderName, pageSize, currentPageIndex, currentPageOffset, sortInfo, null, 360 quickFilters, props, searchDocumentModel, parameters); 361 } 362 363 @SuppressWarnings("unchecked") 364 protected DocumentModelList queryByPageProvider(String pageProviderName, Long pageSize, Long currentPageIndex, 365 Long currentPageOffset, List<SortInfo> sortInfo, List<String> highlights, List<QuickFilter> quickFilters, 366 Map<String, Serializable> props, DocumentModel searchDocumentModel, Object... parameters) { 367 PaginableDocumentModelListImpl res = new PaginableDocumentModelListImpl( 368 (PageProvider<DocumentModel>) pageProviderService.getPageProvider(pageProviderName, searchDocumentModel, 369 sortInfo, pageSize, currentPageIndex, currentPageOffset, props, highlights, quickFilters, 370 parameters), 371 null); 372 if (res.hasError()) { 373 throw new NuxeoException(res.getErrorMessage(), SC_BAD_REQUEST); 374 } 375 return res; 376 } 377 378 protected PageProviderDefinition getPageProviderDefinition(String pageProviderName) { 379 return pageProviderService.getPageProviderDefinition(pageProviderName); 380 } 381 382 protected Response buildResponse(Response.StatusType status, String type, Object object) throws IOException { 383 ObjectMapper mapper = new ObjectMapper(); 384 String message = mapper.writeValueAsString(object); 385 return Response.status(status) 386 .header("Content-Length", message.getBytes("UTF-8").length) 387 .type(type + "; charset=UTF-8") 388 .entity(message) 389 .build(); 390 } 391 392 protected List<String> asStringList(String value) { 393 return StringUtils.isBlank(value) ? null : Arrays.asList(value.split(",")); 394 } 395}