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