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