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