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