001/* 002 * (C) Copyright 2010-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 * Anahide Tchertchian 018 * Kevin Leturc <[email protected]> 019 */ 020package org.nuxeo.ecm.platform.query.nxql; 021 022import java.text.DateFormat; 023import java.text.SimpleDateFormat; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Date; 027import java.util.GregorianCalendar; 028import java.util.List; 029import java.util.Map; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import org.apache.commons.lang3.StringUtils; 034import org.nuxeo.ecm.core.api.DocumentModel; 035import org.nuxeo.ecm.core.api.NuxeoException; 036import org.nuxeo.ecm.core.api.PropertyException; 037import org.nuxeo.ecm.core.api.SortInfo; 038import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 039import org.nuxeo.ecm.core.query.sql.NXQL; 040import org.nuxeo.ecm.core.query.sql.model.Literal; 041import org.nuxeo.ecm.core.schema.SchemaManager; 042import org.nuxeo.ecm.core.schema.types.Field; 043import org.nuxeo.ecm.core.schema.types.Schema; 044import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl; 045import org.nuxeo.ecm.core.schema.types.Type; 046import org.nuxeo.ecm.core.schema.types.primitives.StringType; 047import org.nuxeo.ecm.core.search.api.client.querymodel.Escaper; 048import org.nuxeo.ecm.platform.query.api.PageProviderService; 049import org.nuxeo.ecm.platform.query.api.PredicateDefinition; 050import org.nuxeo.ecm.platform.query.api.PredicateFieldDefinition; 051import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition; 052import org.nuxeo.ecm.platform.query.core.FieldDescriptor; 053import org.nuxeo.runtime.api.Framework; 054import org.nuxeo.runtime.services.config.ConfigurationService; 055 056/** 057 * Helper to generate NXQL queries from XMap descriptors 058 * 059 * @since 5.4 060 * @author Anahide Tchertchian 061 */ 062public class NXQLQueryBuilder { 063 064 // @since 5.9.2 065 public static final String DEFAULT_SELECT_STATEMENT = "SELECT * FROM Document"; 066 067 // @since 5.9 068 public static final String SORTED_COLUMN = "SORTED_COLUMN"; 069 070 public static final String REGEXP_NAMED_PARAMETER = "[^a-zA-Z]:\\s*" + "([a-zA-Z0-9:]*)"; 071 072 public static final String REGEXP_EXCLUDE_QUOTE = "'[^']*'"; 073 074 public static final String REGEXP_EXCLUDE_DOUBLE_QUOTE = "\"[^\"]*\""; 075 076 private NXQLQueryBuilder() { 077 } 078 079 /** 080 * @return the built sort clause from input parameters, always non null 081 */ 082 public static String getSortClause(SortInfo... sortInfos) { 083 StringBuilder queryBuilder = new StringBuilder(); 084 if (sortInfos != null) { 085 int index = 0; 086 for (SortInfo sortInfo : sortInfos) { 087 String sortColumn = sortInfo.getSortColumn(); 088 boolean sortAscending = sortInfo.getSortAscending(); 089 if (index == 0) { 090 queryBuilder.append("ORDER BY ").append(sortColumn).append(' ').append(sortAscending ? "" : "DESC"); 091 } else { 092 queryBuilder.append(", ").append(sortColumn).append(' ').append(sortAscending ? "" : "DESC"); 093 } 094 index++; 095 } 096 } 097 return queryBuilder.toString(); 098 } 099 100 public static String getQuery(DocumentModel model, WhereClauseDefinition whereClause, Object[] params, 101 SortInfo... sortInfos) { 102 return getQuery(model, whereClause, null, params, sortInfos); 103 } 104 105 /** 106 * @since 8.4 107 */ 108 public static String getQuery(DocumentModel model, WhereClauseDefinition whereClause, String quickFiltersClause, 109 Object[] params, SortInfo... sortInfos) { 110 StringBuilder queryBuilder = new StringBuilder(); 111 String selectStatement = whereClause.getSelectStatement(); 112 if (StringUtils.isBlank(selectStatement)) { 113 selectStatement = DEFAULT_SELECT_STATEMENT; 114 } 115 queryBuilder.append(selectStatement); 116 queryBuilder.append(getQueryElement(model, whereClause, quickFiltersClause, params)); 117 118 String sortClause = getSortClause(sortInfos); 119 if (sortClause.length() > 0) { 120 queryBuilder.append(" "); 121 queryBuilder.append(sortClause); 122 } 123 return queryBuilder.toString().trim(); 124 } 125 126 public static String getQueryElement(DocumentModel model, WhereClauseDefinition whereClause, Object[] params) { 127 return getQueryElement(model, whereClause, null, params); 128 } 129 130 /** 131 * @since 8.4 132 */ 133 public static String getQueryElement(DocumentModel model, WhereClauseDefinition whereClause, 134 String quickFiltersClause, Object[] params) { 135 List<String> elements = new ArrayList<>(); 136 PredicateDefinition[] predicates = whereClause.getPredicates(); 137 if (predicates != null) { 138 Escaper escaper = null; 139 Class<? extends Escaper> escaperClass = whereClause.getEscaperClass(); 140 if (escaperClass != null) { 141 try { 142 escaper = escaperClass.newInstance(); 143 } catch (ReflectiveOperationException e) { 144 throw new NuxeoException(e); 145 } 146 } 147 for (PredicateDefinition predicate : predicates) { 148 String predicateString = getQueryElement(model, predicate, escaper); 149 if (predicateString == null) { 150 continue; 151 } 152 153 predicateString = predicateString.trim(); 154 if (!predicateString.equals("")) { 155 elements.add(predicateString); 156 } 157 } 158 } 159 // add fixed part if applicable 160 String fixedPart = whereClause.getFixedPart(); 161 if (!StringUtils.isBlank(fixedPart)) { 162 if (StringUtils.isNotBlank(quickFiltersClause)) { 163 fixedPart = appendClause(fixedPart, quickFiltersClause); 164 } 165 if (elements.isEmpty()) { 166 elements.add(getQuery(fixedPart, params, whereClause.getQuoteFixedPartParameters(), 167 whereClause.getEscapeFixedPartParameters(), model)); 168 } else { 169 elements.add('(' + getQuery(fixedPart, params, whereClause.getQuoteFixedPartParameters(), 170 whereClause.getEscapeFixedPartParameters(), model) + ')'); 171 } 172 } else if (StringUtils.isNotBlank(quickFiltersClause)) { 173 fixedPart = quickFiltersClause; 174 } 175 176 if (elements.isEmpty()) { 177 return ""; 178 } 179 180 // XXX: for now only a one level implement conjunctive WHERE clause 181 String clauseValues = StringUtils.join(elements, " AND ").trim(); 182 183 // GR: WHERE (x = 1) is invalid NXQL 184 while (elements.size() == 1 && clauseValues.startsWith("(") && clauseValues.endsWith(")")) { 185 clauseValues = clauseValues.substring(1, clauseValues.length() - 1).trim(); 186 } 187 if (clauseValues.length() == 0) { 188 return ""; 189 } 190 return " WHERE " + clauseValues; 191 } 192 193 public static String getQuery(String pattern, Object[] params, boolean quoteParameters, boolean escape, 194 DocumentModel searchDocumentModel, SortInfo... sortInfos) { 195 String sortedColumn; 196 if (sortInfos == null || sortInfos.length == 0) { 197 // If there is no ORDER BY use the id 198 sortedColumn = NXQL.ECM_UUID; 199 } else { 200 sortedColumn = sortInfos[0].getSortColumn(); 201 } 202 if (pattern != null && pattern.contains(SORTED_COLUMN)) { 203 pattern = pattern.replace(SORTED_COLUMN, sortedColumn); 204 } 205 StringBuilder queryBuilder; 206 207 // handle named parameters replacements 208 if (searchDocumentModel != null) { 209 // Find all query named parameters as ":parameter" not between 210 // quotes and add them to matches 211 String query = pattern.replaceAll(REGEXP_EXCLUDE_DOUBLE_QUOTE, StringUtils.EMPTY); 212 query = query.replaceAll(REGEXP_EXCLUDE_QUOTE, StringUtils.EMPTY); 213 Pattern p1 = Pattern.compile(REGEXP_NAMED_PARAMETER); 214 Matcher m1 = p1.matcher(query); 215 List<String> matches = new ArrayList<>(); 216 while (m1.find()) { 217 matches.add(m1.group().substring(m1.group().indexOf(":") + 1)); 218 } 219 for (String key : matches) { 220 Object parameter = getRawValue(searchDocumentModel, new FieldDescriptor(key)); 221 if (parameter == null) { 222 continue; 223 } 224 key = ":" + key; 225 if (parameter instanceof String[]) { 226 pattern = replaceStringList(pattern, Arrays.asList((String[]) parameter), quoteParameters, escape, key); 227 } else if (parameter instanceof List) { 228 pattern = replaceStringList(pattern, (List<?>) parameter, quoteParameters, escape, key); 229 } else if (parameter instanceof Boolean) { 230 pattern = buildPattern(pattern, key, ((Boolean) parameter) ? "1" : "0"); 231 } else if (parameter instanceof Number) { 232 pattern = buildPattern(pattern, key, parameter.toString()); 233 } else if (parameter instanceof Literal) { 234 if (quoteParameters) { 235 pattern = buildPattern(pattern, key, "'" + parameter.toString() + "'"); 236 } else { 237 pattern = buildPattern(pattern, key, ((Literal) parameter).asString()); 238 } 239 } else { 240 if (quoteParameters) { 241 pattern = buildPattern(pattern, key, "'" + parameter + "'"); 242 } else { 243 pattern = buildPattern(pattern, key, parameter.toString()); 244 } 245 } 246 } 247 } 248 249 if (params == null) { 250 queryBuilder = new StringBuilder(pattern + ' '); 251 } else { 252 // handle "standard" parameters replacements (referenced by ? characters) 253 // XXX: the + " " is a workaround for the buggy implementation 254 // of the split function in case the pattern ends with '?' 255 String[] queryStrList = (pattern + ' ').split("\\?"); 256 queryBuilder = new StringBuilder(queryStrList[0]); 257 for (int i = 0; i < params.length; i++) { 258 if (params[i] instanceof String[]) { 259 appendStringList(queryBuilder, Arrays.asList((String[]) params[i]), quoteParameters, escape); 260 } else if (params[i] instanceof List) { 261 appendStringList(queryBuilder, (List<?>) params[i], quoteParameters, escape); 262 } else if (params[i] instanceof Boolean) { 263 boolean b = ((Boolean) params[i]).booleanValue(); 264 queryBuilder.append(b ? 1 : 0); 265 } else if (params[i] instanceof Number) { 266 queryBuilder.append(params[i]); 267 } else if (params[i] instanceof Literal) { 268 if (quoteParameters) { 269 queryBuilder.append(params[i].toString()); 270 } else { 271 queryBuilder.append(((Literal) params[i]).asString()); 272 } 273 } else { 274 if (params[i] == null) { 275 if (quoteParameters) { 276 queryBuilder.append("''"); 277 } 278 } else { 279 String queryParam = params[i].toString(); 280 queryBuilder.append(prepareStringLiteral(queryParam, quoteParameters, escape)); 281 } 282 } 283 queryBuilder.append(queryStrList[i + 1]); 284 } 285 } 286 queryBuilder.append(getSortClause(sortInfos)); 287 return queryBuilder.toString().trim(); 288 } 289 290 public static void appendStringList(StringBuilder queryBuilder, List<?> listParam, boolean quoteParameters, 291 boolean escape) { 292 // avoid appending parentheses if the query builder ends with one 293 boolean addParentheses = !queryBuilder.toString().endsWith("("); 294 if (addParentheses) { 295 queryBuilder.append('('); 296 } 297 List<String> result = new ArrayList<>(listParam.size()); 298 for (Object param : listParam) { 299 result.add(prepareStringLiteral(param.toString(), quoteParameters, escape)); 300 } 301 queryBuilder.append(String.join(", ", result)); 302 if (addParentheses) { 303 queryBuilder.append(')'); 304 } 305 } 306 307 public static String replaceStringList(String pattern, List<?> listParams, boolean quoteParameters, boolean escape, 308 String key) { 309 List<String> result = new ArrayList<>(listParams.size()); 310 for (Object param : listParams) { 311 result.add(prepareStringLiteral(param.toString(), quoteParameters, escape)); 312 } 313 314 return buildPattern(pattern, key, '(' + StringUtils.join(result, ", " + "") + ')'); 315 } 316 317 /** 318 * Return the string literal in a form ready to embed in an NXQL statement. 319 */ 320 public static String prepareStringLiteral(String s, boolean quoteParameter, boolean escape) { 321 if (escape) { 322 if (quoteParameter) { 323 return NXQL.escapeString(s); 324 } else { 325 return NXQL.escapeStringInner(s); 326 } 327 } else { 328 if (quoteParameter) { 329 return "'" + s + "'"; 330 } else { 331 return s; 332 } 333 } 334 } 335 336 public static String getQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor, Escaper escaper) { 337 String type = predicateDescriptor.getType(); 338 if (PredicateDefinition.ATOMIC_PREDICATE.equals(type)) { 339 return atomicQueryElement(model, predicateDescriptor, escaper); 340 } 341 if (PredicateDefinition.SUB_CLAUSE_PREDICATE.equals(type)) { 342 return subClauseQueryElement(model, predicateDescriptor); 343 } 344 throw new NuxeoException("Unknown predicate type: " + type); 345 } 346 347 protected static String subClauseQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor) { 348 PredicateFieldDefinition[] values = predicateDescriptor.getValues(); 349 if (values == null || values.length != 1) { 350 throw new NuxeoException("subClause predicate needs exactly one field"); 351 } 352 PredicateFieldDefinition fieldDescriptor = values[0]; 353 if (!getFieldType(model, fieldDescriptor).equals("string")) { 354 if (fieldDescriptor.getXpath() != null) { 355 throw new NuxeoException(String.format("type of field %s is not string", fieldDescriptor.getXpath())); 356 } else { 357 throw new NuxeoException(String.format("type of field %s.%s is not string", 358 fieldDescriptor.getSchema(), fieldDescriptor.getName())); 359 } 360 } 361 Object subclauseValue = getRawValue(model, fieldDescriptor); 362 if (subclauseValue == null) { 363 return ""; 364 } 365 366 return "(" + subclauseValue + ")"; 367 } 368 369 protected static String atomicQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor, 370 Escaper escaper) { 371 String operator = null; 372 String operatorField = predicateDescriptor.getOperatorField(); 373 String operatorSchema = predicateDescriptor.getOperatorSchema(); 374 PredicateFieldDefinition[] values = predicateDescriptor.getValues(); 375 if (operatorField != null && operatorSchema != null) { 376 PredicateFieldDefinition operatorFieldDescriptor = new FieldDescriptor(operatorSchema, operatorField); 377 operator = getPlainStringValue(model, operatorFieldDescriptor); 378 if (operator != null) { 379 operator = operator.toUpperCase(); 380 } 381 } 382 if (StringUtils.isBlank(operator)) { 383 operator = predicateDescriptor.getOperator(); 384 } 385 String hint = predicateDescriptor.getHint(); 386 String parameter = getParameterWithHint(operator, predicateDescriptor.getParameter(), hint); 387 388 if (operator.equals("=") || operator.equals("!=") || operator.equals("<") || operator.equals(">") 389 || operator.equals("<=") || operator.equals(">=") || operator.equals("<>") || operator.equals("LIKE") 390 || operator.equals("ILIKE")) { 391 // Unary predicate 392 String value = getStringValue(model, values[0]); 393 if (value == null) { 394 // value not provided: ignore predicate 395 return ""; 396 } 397 if (escaper != null && (operator.equals("LIKE") || operator.equals("ILIKE"))) { 398 value = escaper.escape(value); 399 } 400 return serializeUnary(parameter, operator, value); 401 402 } else if (operator.equals("BETWEEN")) { 403 String min = getStringValue(model, values[0]); 404 String max = getStringValue(model, values[1]); 405 406 if (min != null && max != null) { 407 StringBuilder builder = new StringBuilder(); 408 builder.append(parameter); 409 builder.append(' '); 410 builder.append(operator); 411 builder.append(' '); 412 builder.append(min); 413 builder.append(" AND "); 414 builder.append(max); 415 return builder.toString(); 416 } else if (max != null) { 417 return serializeUnary(parameter, "<=", max); 418 } else if (min != null) { 419 return serializeUnary(parameter, ">=", min); 420 } else { 421 // both min and max are not provided, ignore predicate 422 return ""; 423 } 424 } else if (operator.equals("IN") || operator.equals("NOT IN")) { 425 List<String> options = getListValue(model, values[0]); 426 if (options == null || options.isEmpty()) { 427 return ""; 428 } else if (options.size() == 1) { 429 if (operator.equals("NOT IN")) { 430 return serializeUnary(parameter, "!=", options.get(0)); 431 } else { 432 return serializeUnary(parameter, "=", options.get(0)); 433 } 434 } else { 435 StringBuilder builder = new StringBuilder(); 436 builder.append(parameter); 437 if (operator.equals("NOT IN")) { 438 builder.append(" NOT IN ("); 439 } else { 440 builder.append(" IN ("); 441 } 442 for (int i = 0; i < options.size(); i++) { 443 if (i != 0) { 444 builder.append(", "); 445 } 446 builder.append(options.get(i)); 447 } 448 builder.append(')'); 449 return builder.toString(); 450 } 451 } else if (operator.equals("STARTSWITH")) { 452 String fieldType = getFieldType(model, values[0]); 453 if (fieldType.equals("string")) { 454 String value = getStringValue(model, values[0]); 455 if (value == null) { 456 return ""; 457 } else { 458 return serializeUnary(parameter, operator, value); 459 } 460 } else { 461 List<String> options = getListValue(model, values[0]); 462 if (options == null || options.isEmpty()) { 463 return ""; 464 } else if (options.size() == 1) { 465 return serializeUnary(parameter, operator, options.get(0)); 466 } else { 467 StringBuilder builder = new StringBuilder(); 468 builder.append('('); 469 for (int i = 0; i < options.size() - 1; i++) { 470 builder.append(serializeUnary(parameter, operator, options.get(i))); 471 builder.append(" OR "); 472 } 473 builder.append(serializeUnary(parameter, operator, options.get(options.size() - 1))); 474 builder.append(')'); 475 return builder.toString(); 476 } 477 } 478 } else if (operator.equals("EMPTY") || operator.equals("ISEMPTY")) { 479 return parameter + " = ''"; 480 } else if (operator.equals("FULLTEXT ALL") // BBB 481 || operator.equals("FULLTEXT")) { 482 String value = getPlainStringValue(model, values[0]); 483 if (value == null) { 484 // value not provided: ignore predicate 485 return ""; 486 } 487 if (escaper != null) { 488 value = escaper.escape(value); 489 } 490 return parameter + ' ' + serializeFullText(value); 491 } else if (operator.equals("IS NULL")) { 492 Boolean value = getBooleanValue(model, values[0]); 493 if (value == null) { 494 // value not provided: ignore predicate 495 return ""; 496 } else if (Boolean.TRUE.equals(value)) { 497 return parameter + " IS NULL"; 498 } else { 499 return parameter + " IS NOT NULL"; 500 } 501 } else { 502 throw new NuxeoException("Unsupported operator: " + operator); 503 } 504 } 505 506 protected static String getParameterWithHint(String operator, String parameter, String hint) { 507 String ret = parameter; 508 // add ecm:fulltext. prefix if needed 509 if ((operator.equals("FULLTEXT ALL") || operator.equals("FULLTEXT")) 510 && !parameter.startsWith(NXQL.ECM_FULLTEXT)) { 511 ret = NXQL.ECM_FULLTEXT + '.' + parameter; 512 } 513 // add the hint 514 if (hint != null && !hint.isEmpty()) { 515 ret = String.format("/*+%s */ %s", hint.trim(), ret); 516 } 517 return ret; 518 } 519 520 public static final String DEFAULT_SPECIAL_CHARACTERS_REGEXP = "!#$%&'()+,./\\\\:-@{|}`^~"; 521 522 public static final String IGNORED_CHARS_KEY = "org.nuxeo.query.builder.ignored.chars"; 523 524 /** 525 * Remove any special character that could be mis-interpreted as a low level full-text query operator. This method 526 * should be used by user facing callers of CoreQuery*PageProviders that use a fixed part or a pattern query. Fields 527 * in where clause already dealt with. 528 * 529 * @since 5.6 530 * @return sanitized text value 531 */ 532 public static String sanitizeFulltextInput(String value) { 533 // Ideally the low level full-text language 534 // parser should be robust to any user input however this is much more 535 // complicated to implement correctly than the following simple user 536 // input filtering scheme. 537 ConfigurationService cs = Framework.getService(ConfigurationService.class); 538 String ignoredChars = cs.getProperty(IGNORED_CHARS_KEY, DEFAULT_SPECIAL_CHARACTERS_REGEXP); 539 String res = ""; 540 value = value.replaceAll("[" + ignoredChars + "]", " "); 541 value = value.trim(); 542 String[] tokens = value.split("[\\s]+"); 543 for (String token : tokens) { 544 if ("-".equals(token) || "*".equals(token) || "*-".equals(token) || "-*".equals(token)) { 545 continue; 546 } 547 if (res.length() > 0) { 548 res += " "; 549 } 550 if (token.startsWith("-") || token.endsWith("*")) { 551 res += token; 552 } else { 553 res += token.replace("-", " ").replace("*", " "); 554 } 555 } 556 return res.trim(); 557 } 558 559 public static String serializeFullText(String value) { 560 value = sanitizeFulltextInput(value); 561 return "= " + NXQL.escapeString(value); 562 } 563 564 protected static String serializeUnary(String parameter, String operator, String rvalue) { 565 StringBuilder builder = new StringBuilder(); 566 builder.append(parameter); 567 builder.append(' '); 568 builder.append(operator); 569 builder.append(' '); 570 builder.append(rvalue); 571 return builder.toString(); 572 } 573 574 public static String getPlainStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 575 Object rawValue = getRawValue(model, fieldDescriptor); 576 if (rawValue == null) { 577 return null; 578 } 579 String value = (String) rawValue; 580 if (value.equals("")) { 581 return null; 582 } 583 return value; 584 } 585 586 public static Integer getIntValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 587 Object rawValue = getRawValue(model, fieldDescriptor); 588 if (rawValue == null || "".equals(rawValue)) { 589 return null; 590 } else if (rawValue instanceof Integer) { 591 return (Integer) rawValue; 592 } else if (rawValue instanceof String) { 593 return Integer.valueOf((String) rawValue); 594 } else { 595 return Integer.valueOf(rawValue.toString()); 596 } 597 } 598 599 public static String getFieldType(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 600 String xpath = fieldDescriptor.getXpath(); 601 String schema = fieldDescriptor.getSchema(); 602 String name = fieldDescriptor.getName(); 603 try { 604 SchemaManager typeManager = Framework.getService(SchemaManager.class); 605 Field field = null; 606 if (xpath != null) { 607 if (model != null) { 608 field = model.getProperty(xpath).getField(); 609 } 610 } else { 611 if (schema != null) { 612 Schema schemaObj = typeManager.getSchema(schema); 613 if (schemaObj == null) { 614 throw new NuxeoException("failed to obtain schema: " + schema); 615 } 616 field = schemaObj.getField(name); 617 } else { 618 // assume named parameter use case: hard-code on String in this case 619 return StringType.ID; 620 } 621 } 622 if (field == null) { 623 throw new NuxeoException("failed to obtain field: " + schema + ":" + name); 624 } 625 Type type = field.getType(); 626 if (type instanceof SimpleTypeImpl) { 627 // type with constraint 628 type = type.getSuperType(); 629 } 630 return type.getName(); 631 } catch (PropertyException e) { 632 e.addInfo("failed to get field type for " + (xpath != null ? xpath : (schema + ":" + name))); 633 throw e; 634 } 635 } 636 637 @SuppressWarnings("unchecked") 638 public static Object getRawValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 639 String xpath = fieldDescriptor.getXpath(); 640 String schema = fieldDescriptor.getSchema(); 641 String name = fieldDescriptor.getName(); 642 try { 643 if (xpath != null) { 644 return model.getPropertyValue(xpath); 645 } else if (schema == null) { 646 return model.getPropertyValue(name); 647 } else { 648 return model.getProperty(schema, name); 649 } 650 } catch (PropertyNotFoundException e) { 651 // fall back on named parameters if any 652 Map<String, Object> params = (Map<String, Object>) model.getContextData( 653 PageProviderService.NAMED_PARAMETERS); 654 if (params != null) { 655 if (xpath != null) { 656 return params.get(xpath); 657 } else { 658 return params.get(name); 659 } 660 } 661 } catch (PropertyException e) { 662 return null; 663 } 664 return null; 665 } 666 667 public static String getStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 668 Object rawValue = getRawValue(model, fieldDescriptor); 669 if (rawValue == null) { 670 return null; 671 } 672 String value; 673 if (rawValue instanceof GregorianCalendar) { 674 GregorianCalendar gc = (GregorianCalendar) rawValue; 675 value = "TIMESTAMP '" + getDateFormat().format(gc.getTime()) + "'"; 676 } else if (rawValue instanceof Date) { 677 Date date = (Date) rawValue; 678 value = "TIMESTAMP '" + getDateFormat().format(date) + "'"; 679 } else if (rawValue instanceof Integer || rawValue instanceof Long || rawValue instanceof Double) { 680 value = rawValue.toString(); // no quotes 681 } else if (rawValue instanceof Boolean) { 682 value = ((Boolean) rawValue).booleanValue() ? "1" : "0"; 683 } else { 684 value = rawValue.toString().trim(); 685 if (value.equals("")) { 686 return null; 687 } 688 String fieldType = getFieldType(model, fieldDescriptor); 689 if ("long".equals(fieldType) || "integer".equals(fieldType) || "double".equals(fieldType)) { 690 return value; 691 } else { 692 return NXQL.escapeString(value); 693 } 694 } 695 return value; 696 } 697 698 protected static DateFormat getDateFormat() { 699 // not thread-safe so don't use a static instance 700 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 701 } 702 703 @SuppressWarnings("unchecked") 704 public static List<String> getListValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 705 Object rawValue = getRawValue(model, fieldDescriptor); 706 if (rawValue == null) { 707 return null; 708 } 709 List<String> values = new ArrayList<>(); 710 if (rawValue instanceof ArrayList) { 711 rawValue = ((ArrayList<Object>) rawValue).toArray(); 712 } 713 for (Object element : (Object[]) rawValue) { 714 if (element != null) { 715 if (element instanceof Number) { 716 values.add(element.toString()); 717 } else { 718 String value = element.toString().trim(); 719 if (!value.equals("")) { 720 values.add(NXQL.escapeString(value)); 721 } 722 } 723 } 724 } 725 return values; 726 } 727 728 public static Boolean getBooleanValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 729 Object rawValue = getRawValue(model, fieldDescriptor); 730 if (rawValue == null) { 731 return null; 732 } else { 733 return (Boolean) rawValue; 734 } 735 } 736 737 /** 738 * @since 8.4 739 */ 740 public static String appendClause(String query, String clause) { 741 return query + " AND " + clause; 742 } 743 744 /** 745 * @since 8.4 746 */ 747 public static String buildPattern(String pattern, String key, String replacement) { 748 int index = pattern.indexOf(key); 749 while (index >= 0) { 750 // All keys not prefixed by a letter or a digit has to be replaced, because 751 // It could be part of a schema name 752 if (!Character.isLetterOrDigit(pattern.charAt(index - 1)) && (index + key.length() == pattern.length() 753 || !Character.isLetterOrDigit(pattern.charAt(index + key.length())))) { 754 pattern = pattern.substring(0, index) + pattern.substring(index).replaceFirst(key, replacement); 755 } 756 index = pattern.indexOf(key, index + 1); 757 } 758 return pattern; 759 } 760 761}