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