001/* 002 * (C) Copyright 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 * Nelson Silva <[email protected]> 018 */ 019package org.nuxeo.ecm.automation.core.util; 020 021import static org.nuxeo.ecm.platform.query.api.PageProviderService.NAMED_PARAMETERS; 022 023import org.apache.commons.lang3.ArrayUtils; 024import org.apache.commons.lang3.StringUtils; 025 026import org.joda.time.format.DateTimeFormatter; 027import org.joda.time.format.ISODateTimeFormat; 028import org.nuxeo.ecm.core.api.CoreSession; 029import org.nuxeo.ecm.core.api.DocumentModel; 030import org.nuxeo.ecm.core.api.NuxeoException; 031import org.nuxeo.ecm.core.api.SortInfo; 032import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel; 033import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 034import org.nuxeo.ecm.core.schema.SchemaManager; 035import org.nuxeo.ecm.core.schema.types.Type; 036import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 037import org.nuxeo.ecm.core.schema.types.primitives.LongType; 038import org.nuxeo.ecm.platform.actions.ELActionContext; 039import org.nuxeo.ecm.platform.el.ELService; 040import org.nuxeo.ecm.platform.query.api.Aggregate; 041import org.nuxeo.ecm.platform.query.api.Bucket; 042import org.nuxeo.ecm.platform.query.api.PageProvider; 043import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 044import org.nuxeo.ecm.platform.query.api.PageProviderService; 045import org.nuxeo.ecm.platform.query.api.QuickFilter; 046import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition; 047import org.nuxeo.ecm.platform.query.core.BucketRange; 048import org.nuxeo.ecm.platform.query.core.BucketRangeDate; 049import org.nuxeo.ecm.platform.query.core.BucketTerm; 050import org.nuxeo.ecm.platform.query.core.CoreQueryPageProviderDescriptor; 051import org.nuxeo.ecm.platform.query.core.GenericPageProviderDescriptor; 052import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; 053import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider; 054import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; 055import org.nuxeo.runtime.api.Framework; 056 057import javax.el.ELContext; 058import javax.el.ValueExpression; 059import java.io.IOException; 060import java.io.Serializable; 061import java.util.ArrayList; 062import java.util.HashMap; 063import java.util.List; 064import java.util.Map; 065import java.util.stream.Collectors; 066import java.util.stream.StreamSupport; 067 068import com.fasterxml.jackson.databind.JsonNode; 069import com.fasterxml.jackson.databind.ObjectMapper; 070 071/** 072 * @since 10.3 073 */ 074public class PageProviderHelper { 075 076 final static class QueryAndFetchProviderDescriptor extends GenericPageProviderDescriptor { 077 private static final long serialVersionUID = 1L; 078 079 public QueryAndFetchProviderDescriptor() { 080 super(); 081 try { 082 klass = (Class<PageProvider<?>>) Class.forName(CoreQueryAndFetchPageProvider.class.getName()); 083 } catch (ClassNotFoundException e) { 084 // log.error(e, e); 085 } 086 } 087 } 088 089 public static final String ASC = "ASC"; 090 091 public static final String DESC = "DESC"; 092 093 public static final String CURRENT_USERID_PATTERN = "$currentUser"; 094 095 public static final String CURRENT_REPO_PATTERN = "$currentRepository"; 096 097 protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 098 099 protected static final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime(); 100 101 public static PageProviderDefinition getQueryAndFetchProviderDefinition(String query) { 102 return getQueryAndFetchProviderDefinition(query, null); 103 } 104 105 public static PageProviderDefinition getQueryAndFetchProviderDefinition(String query, Map<String, String> properties) { 106 QueryAndFetchProviderDescriptor desc = new QueryAndFetchProviderDescriptor(); 107 desc.setName(StringUtils.EMPTY); 108 desc.setPattern(query); 109 if (properties != null) { 110 // set the maxResults to avoid slowing down queries 111 desc.getProperties().putAll(properties); 112 } 113 return desc; 114 } 115 116 public static PageProviderDefinition getQueryPageProviderDefinition(String query) { 117 return getQueryPageProviderDefinition(query, null); 118 } 119 120 public static PageProviderDefinition getQueryPageProviderDefinition(String query, Map<String, String> properties) { 121 CoreQueryPageProviderDescriptor desc = new CoreQueryPageProviderDescriptor(); 122 desc.setName(StringUtils.EMPTY); 123 desc.setPattern(query); 124 if (properties != null) { 125 // set the maxResults to avoid slowing down queries 126 desc.getProperties().putAll(properties); 127 } 128 return desc; 129 } 130 131 public static PageProviderDefinition getPageProviderDefinition(String providerName) { 132 PageProviderService pageProviderService = Framework.getService(PageProviderService.class); 133 return pageProviderService.getPageProviderDefinition(providerName); 134 } 135 136 public static PageProvider<?> getPageProvider(CoreSession session, PageProviderDefinition def, 137 Map<String, String> namedParameters, Object... queryParams) { 138 return getPageProvider(session, def, namedParameters, null, null, null, null, queryParams); 139 } 140 141 public static PageProvider<?> getPageProvider(CoreSession session, PageProviderDefinition def, 142 Map<String, String> namedParameters, List<String> sortBy, List<String> sortOrder, 143 Long pageSize, Long currentPageIndex, Object... queryParams) { 144 return getPageProvider(session, def, namedParameters, sortBy, sortOrder, pageSize, currentPageIndex, 145 null, null, queryParams); 146 } 147 148 public static PageProvider<?> getPageProvider(CoreSession session, PageProviderDefinition def, 149 Map<String, String> namedParameters, List<String> sortBy, List<String> sortOrder, 150 Long pageSize, Long currentPageIndex, List<String> highlights, List<String> quickFilters, 151 Object... parameters) { 152 153 // Ordered parameters 154 if (ArrayUtils.isNotEmpty(parameters)) { 155 // expand specific parameters 156 for (int idx = 0; idx < parameters.length; idx++) { 157 String value = (String) parameters[idx]; 158 if (value.equals(CURRENT_USERID_PATTERN)) { 159 parameters[idx] = session.getPrincipal().getName(); 160 } else if (value.equals(CURRENT_REPO_PATTERN)) { 161 parameters[idx] = session.getRepositoryName(); 162 } 163 } 164 } 165 166 // Sort Info Management 167 List<SortInfo> sortInfos = null; 168 if (sortBy != null) { 169 sortInfos = new ArrayList<>(); 170 for (int i = 0; i < sortBy.size(); i++) { 171 String sort = sortBy.get(i); 172 if (StringUtils.isNotBlank(sort)) { 173 boolean sortAscending = (sortOrder != null && !sortOrder.isEmpty() && ASC.equalsIgnoreCase( 174 sortOrder.get(i).toLowerCase())); 175 sortInfos.add(new SortInfo(sort, sortAscending)); 176 } 177 } 178 } 179 180 // Quick filters management 181 List<QuickFilter> quickFilterList = null; 182 if (quickFilters != null) { 183 quickFilterList = new ArrayList<>(); 184 for (String filter : quickFilters) { 185 for (QuickFilter quickFilter : def.getQuickFilters()) { 186 if (quickFilter.getName().equals(filter)) { 187 quickFilterList.add(quickFilter); 188 break; 189 } 190 } 191 } 192 } 193 194 Map<String, Serializable> props = new HashMap<>(); 195 props.put(CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY, (Serializable) session); 196 DocumentModel searchDocumentModel = getSearchDocumentModel(session, def.getName(), namedParameters); 197 198 PageProviderService pageProviderService = Framework.getService(PageProviderService.class); 199 200 return pageProviderService.getPageProvider(def.getName(), def, 201 searchDocumentModel, sortInfos, pageSize, currentPageIndex, props, highlights, quickFilterList, parameters); 202 } 203 204 public static DocumentModel getSearchDocumentModel(CoreSession session, String providerName, 205 Map<String, String> namedParameters) { 206 PageProviderService pageProviderService = Framework.getService(PageProviderService.class); 207 PageProviderDefinition def = pageProviderService.getPageProviderDefinition(providerName); 208 209 // generate search document model if type specified on the definition 210 DocumentModel searchDocumentModel = null; 211 212 if (def != null) { 213 String searchDocType = def.getSearchDocumentType(); 214 if (searchDocType != null) { 215 searchDocumentModel = session.createDocumentModel(searchDocType); 216 } else if (def.getWhereClause() != null) { 217 // avoid later error on null search doc, in case where clause is only referring to named parameters 218 // (and no namedParameters are given) 219 searchDocumentModel = new SimpleDocumentModel(); 220 } 221 } 222 223 if (namedParameters != null && !namedParameters.isEmpty()) { 224 // fall back on simple document if no type defined on page provider 225 if (searchDocumentModel == null) { 226 searchDocumentModel = new SimpleDocumentModel(); 227 } 228 for (Map.Entry<String, String> entry : namedParameters.entrySet()) { 229 String key = entry.getKey(); 230 String value = entry.getValue(); 231 try { 232 DocumentHelper.setProperty(session, searchDocumentModel, key, value, true); 233 } catch (PropertyNotFoundException | IOException e) { 234 // assume this is a "pure" named parameter, not part of the search doc schema 235 continue; 236 } 237 } 238 searchDocumentModel.putContextData(NAMED_PARAMETERS, (Serializable) namedParameters); 239 } 240 return searchDocumentModel; 241 } 242 243 public static String buildQueryString(PageProvider provider) { 244 return buildQueryStringWithPageProvider(provider, false); 245 } 246 247 public static String buildQueryStringWithAggregates(PageProvider provider) { 248 return buildQueryStringWithPageProvider(provider, provider.hasAggregateSupport()); 249 } 250 251 @SuppressWarnings({ "rawtypes", "unchecked" }) 252 protected static String buildQueryStringWithPageProvider(PageProvider provider, boolean useAggregates) { 253 String quickFiltersClause = ""; 254 List<QuickFilter> quickFilters = provider.getQuickFilters(); 255 if (quickFilters != null) { 256 for (QuickFilter quickFilter : quickFilters) { 257 String clause = quickFilter.getClause(); 258 if (!quickFiltersClause.isEmpty() && clause != null) { 259 quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause); 260 } else { 261 quickFiltersClause = StringUtils.defaultString(clause); 262 } 263 } 264 } 265 266 String aggregatesClause = useAggregates ? buildAggregatesClause(provider) : null; 267 268 PageProviderDefinition def = provider.getDefinition(); 269 WhereClauseDefinition whereClause = def.getWhereClause(); 270 DocumentModel searchDocumentModel = provider.getSearchDocumentModel(); 271 Object[] parameters = provider.getParameters(); 272 String query; 273 if (whereClause == null) { 274 String pattern = def.getPattern(); 275 if (!quickFiltersClause.isEmpty()) { 276 pattern = appendToPattern(pattern, quickFiltersClause); 277 } 278 if (StringUtils.isNotEmpty(aggregatesClause)) { 279 pattern = appendToPattern(pattern, aggregatesClause); 280 } 281 282 query = NXQLQueryBuilder.getQuery(pattern, parameters, def.getQuotePatternParameters(), 283 def.getEscapePatternParameters(), searchDocumentModel, null); 284 } else { 285 if (searchDocumentModel == null) { 286 throw new NuxeoException(String.format( 287 "Cannot build query of provider '%s': " + "no search document model is set", provider.getName())); 288 } 289 String additionalClause = StringUtils.isEmpty(quickFiltersClause) ? aggregatesClause 290 : NXQLQueryBuilder.appendClause(aggregatesClause, quickFiltersClause); 291 query = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, additionalClause, parameters, null); 292 } 293 return query; 294 } 295 296 @SuppressWarnings({ "rawtypes", "unchecked" }) 297 protected static String buildAggregatesClause(PageProvider provider) { 298 try { 299 String aggregatesClause = ""; 300 // Aggregates that are being used as filters are stored in the namedParameters context data 301 Properties namedParameters = (Properties) provider.getSearchDocumentModel() 302 .getContextData(NAMED_PARAMETERS); 303 Map<String, Aggregate<? extends Bucket>> aggregates = provider.getAggregates(); 304 for (Aggregate<? extends Bucket> aggregate : aggregates.values()) { 305 if (namedParameters.containsKey(aggregate.getId())) { 306 JsonNode node = OBJECT_MAPPER.readTree(namedParameters.get(aggregate.getId())); 307 // Remove leading trailing and trailing quotes caused by 308 // the JSON serialization of the named parameters 309 List<String> keys = StreamSupport.stream(node.spliterator(), false) 310 .map(value -> value.asText().replaceAll("^\"|\"$", "")) 311 .collect(Collectors.toList()); 312 // Build aggregate clause from given keys in the named parameters 313 String aggClause = aggregate.getBuckets() 314 .stream() 315 .filter(bucket -> keys.contains(bucket.getKey())) 316 .map(bucket -> getClauseFromBucket(bucket, aggregate.getField())) 317 .collect(Collectors.joining(" OR ")); 318 if (StringUtils.isNotEmpty(aggClause)) { 319 aggClause = "(" + aggClause + ")"; 320 aggregatesClause = StringUtils.isEmpty(aggregatesClause) ? aggClause 321 : NXQLQueryBuilder.appendClause(aggregatesClause, aggClause); 322 } 323 } 324 } 325 return aggregatesClause; 326 } catch (IOException e) { 327 throw new NuxeoException(e); 328 } 329 } 330 331 protected static String getClauseFromBucket(Bucket bucket, String field) { 332 String clause; 333 // Replace potential '.' path separator with '/' character 334 field = field.replaceAll("\\.", "/"); 335 if (bucket instanceof BucketTerm) { 336 clause = field + "='" + bucket.getKey() + "'"; 337 } else if (bucket instanceof BucketRange) { 338 BucketRange bucketRange = (BucketRange) bucket; 339 clause = getRangeClause(field, bucketRange); 340 } else if (bucket instanceof BucketRangeDate) { 341 BucketRangeDate bucketRangeDate = (BucketRangeDate) bucket; 342 clause = getRangeDateClause(field, bucketRangeDate); 343 } else { 344 throw new NuxeoException("Unknown bucket instance for NXQL translation : " + bucket.getClass()); 345 } 346 return clause; 347 } 348 349 protected static String getRangeClause(String field, BucketRange bucketRange) { 350 Type type = Framework.getService(SchemaManager.class).getField(field).getType(); 351 Double from = bucketRange.getFrom() != null ? bucketRange.getFrom() : Double.NEGATIVE_INFINITY; 352 Double to = bucketRange.getTo() != null ? bucketRange.getTo() : Double.POSITIVE_INFINITY; 353 if (type instanceof IntegerType) { 354 return field + " BETWEEN " + from.intValue() + " AND " + to.intValue(); 355 } else if (type instanceof LongType) { 356 return field + " BETWEEN " + from.longValue() + " AND " + to.longValue(); 357 } 358 return field + " BETWEEN " + from + " AND " + to; 359 } 360 361 protected static String getRangeDateClause(String field, BucketRangeDate bucketRangeDate) { 362 Double from = bucketRangeDate.getFrom(); 363 Double to = bucketRangeDate.getTo(); 364 if (from == null && to != null) { 365 return field + " < TIMESTAMP '" + DATE_TIME_FORMATTER.print(bucketRangeDate.getToAsDate()) + "'"; 366 } else if (from != null && to == null) { 367 return field + " >= TIMESTAMP '" + DATE_TIME_FORMATTER.print(bucketRangeDate.getFromAsDate()) + "'"; 368 } 369 return field + " BETWEEN TIMESTAMP '" + DATE_TIME_FORMATTER.print(bucketRangeDate.getFromAsDate()) 370 + "' AND TIMESTAMP '" + DATE_TIME_FORMATTER.print(bucketRangeDate.getToAsDate()) + "'"; 371 } 372 373 protected static String appendToPattern(String pattern, String clause) { 374 return StringUtils.containsIgnoreCase(pattern, " WHERE ") ? NXQLQueryBuilder.appendClause(pattern, clause) 375 : pattern + " WHERE " + clause; 376 } 377 378 /** 379 * Resolves additional parameters that could have been defined in the contribution. 380 * 381 * @param parameters parameters from the operation 382 */ 383 public static Object[] resolveELParameters(PageProviderDefinition def, Object ...parameters) { 384 ELService elService = Framework.getService(ELService.class); 385 if (elService == null) { 386 return parameters; 387 } 388 389 // resolve additional parameters 390 String[] params = def.getQueryParameters(); 391 if (params == null) { 392 params = new String[0]; 393 } 394 395 Object[] resolvedParams = new Object[params.length + (parameters != null ? parameters.length : 0)]; 396 397 ELContext elContext = elService.createELContext(); 398 399 int i = 0; 400 if (parameters != null) { 401 i = parameters.length; 402 System.arraycopy(parameters, 0, resolvedParams, 0, i); 403 } 404 for (int j = 0; j < params.length; j++) { 405 ValueExpression ve = ELActionContext.EXPRESSION_FACTORY.createValueExpression(elContext, params[j], 406 Object.class); 407 resolvedParams[i + j] = ve.getValue(elContext); 408 } 409 return resolvedParams; 410 } 411}