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