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 <kleturc@nuxeo.com> 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.getDeclaredConstructor().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") || operator.equals("NOT LIKE") || operator.equals("NOT 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 || operator.equals("NOT LIKE") || operator.equals("NOT ILIKE"))) { 399 value = escaper.escape(value); 400 } 401 return serializeUnary(parameter, operator, value); 402 403 } else if (operator.equals("BETWEEN")) { 404 String min = getStringValue(model, values[0]); 405 String max = getStringValue(model, values[1]); 406 407 if (min != null && max != null) { 408 StringBuilder builder = new StringBuilder(); 409 builder.append(parameter); 410 builder.append(' '); 411 builder.append(operator); 412 builder.append(' '); 413 builder.append(min); 414 builder.append(" AND "); 415 builder.append(max); 416 return builder.toString(); 417 } else if (max != null) { 418 return serializeUnary(parameter, "<=", max); 419 } else if (min != null) { 420 return serializeUnary(parameter, ">=", min); 421 } else { 422 // both min and max are not provided, ignore predicate 423 return ""; 424 } 425 } else if (operator.equals("IN") || operator.equals("NOT IN")) { 426 List<String> options = getListValue(model, values[0]); 427 if (options == null || options.isEmpty()) { 428 return ""; 429 } else if (options.size() == 1) { 430 if (operator.equals("NOT IN")) { 431 return serializeUnary(parameter, "!=", options.get(0)); 432 } else { 433 return serializeUnary(parameter, "=", options.get(0)); 434 } 435 } else { 436 StringBuilder builder = new StringBuilder(); 437 builder.append(parameter); 438 if (operator.equals("NOT IN")) { 439 builder.append(" NOT IN ("); 440 } else { 441 builder.append(" IN ("); 442 } 443 for (int i = 0; i < options.size(); i++) { 444 if (i != 0) { 445 builder.append(", "); 446 } 447 builder.append(options.get(i)); 448 } 449 builder.append(')'); 450 return builder.toString(); 451 } 452 } else if (operator.equals("STARTSWITH")) { 453 String fieldType = getFieldType(model, values[0]); 454 if (fieldType.equals("string")) { 455 String value = getStringValue(model, values[0]); 456 if (value == null) { 457 return ""; 458 } else { 459 return serializeUnary(parameter, operator, value); 460 } 461 } else { 462 List<String> options = getListValue(model, values[0]); 463 if (options == null || options.isEmpty()) { 464 return ""; 465 } else if (options.size() == 1) { 466 return serializeUnary(parameter, operator, options.get(0)); 467 } else { 468 StringBuilder builder = new StringBuilder(); 469 builder.append('('); 470 for (int i = 0; i < options.size() - 1; i++) { 471 builder.append(serializeUnary(parameter, operator, options.get(i))); 472 builder.append(" OR "); 473 } 474 builder.append(serializeUnary(parameter, operator, options.get(options.size() - 1))); 475 builder.append(')'); 476 return builder.toString(); 477 } 478 } 479 } else if (operator.equals("EMPTY") || operator.equals("ISEMPTY")) { 480 return parameter + " = ''"; 481 } else if (operator.equals("FULLTEXT ALL") // BBB 482 || operator.equals("FULLTEXT")) { 483 String value = getPlainStringValue(model, values[0]); 484 if (value == null) { 485 // value not provided: ignore predicate 486 return ""; 487 } 488 if (escaper != null) { 489 value = escaper.escape(value); 490 } 491 return parameter + ' ' + serializeFullText(value); 492 } else if (operator.equals("IS NULL")) { 493 Boolean value = getBooleanValue(model, values[0]); 494 if (value == null) { 495 // value not provided: ignore predicate 496 return ""; 497 } else if (Boolean.TRUE.equals(value)) { 498 return parameter + " IS NULL"; 499 } else { 500 return parameter + " IS NOT NULL"; 501 } 502 } else { 503 throw new NuxeoException("Unsupported operator: " + operator); 504 } 505 } 506 507 protected static String getParameterWithHint(String operator, String parameter, String hint) { 508 String ret = parameter; 509 // add ecm:fulltext. prefix if needed 510 if ((operator.equals("FULLTEXT ALL") || operator.equals("FULLTEXT")) 511 && !parameter.startsWith(NXQL.ECM_FULLTEXT)) { 512 ret = NXQL.ECM_FULLTEXT + '.' + parameter; 513 } 514 // add the hint 515 if (hint != null && !hint.isEmpty()) { 516 ret = String.format("/*+%s */ %s", hint.trim(), ret); 517 } 518 return ret; 519 } 520 521 public static final String DEFAULT_SPECIAL_CHARACTERS_REGEXP = "!#$%&'()+,./\\\\:-@{|}`^~"; 522 523 public static final String IGNORED_CHARS_KEY = "org.nuxeo.query.builder.ignored.chars"; 524 525 /** 526 * Remove any special character that could be mis-interpreted as a low level full-text query operator. This method 527 * should be used by user facing callers of CoreQuery*PageProviders that use a fixed part or a pattern query. Fields 528 * in where clause already dealt with. 529 * 530 * @since 5.6 531 * @return sanitized text value 532 */ 533 public static String sanitizeFulltextInput(String value) { 534 // Ideally the low level full-text language 535 // parser should be robust to any user input however this is much more 536 // complicated to implement correctly than the following simple user 537 // input filtering scheme. 538 ConfigurationService cs = Framework.getService(ConfigurationService.class); 539 String ignoredChars = cs.getString(IGNORED_CHARS_KEY, DEFAULT_SPECIAL_CHARACTERS_REGEXP); 540 String res = ""; 541 value = value.replaceAll("[" + ignoredChars + "]", " "); 542 value = value.trim(); 543 String[] tokens = value.split("[\\s]+"); 544 for (String token : tokens) { 545 if ("-".equals(token) || "*".equals(token) || "*-".equals(token) || "-*".equals(token)) { 546 continue; 547 } 548 if (res.length() > 0) { 549 res += " "; 550 } 551 if (token.startsWith("-") || token.endsWith("*")) { 552 res += token; 553 } else { 554 res += token.replace("-", " ").replace("*", " "); 555 } 556 } 557 return res.trim(); 558 } 559 560 public static String serializeFullText(String value) { 561 value = sanitizeFulltextInput(value); 562 return "= " + NXQL.escapeString(value); 563 } 564 565 protected static String serializeUnary(String parameter, String operator, String rvalue) { 566 StringBuilder builder = new StringBuilder(); 567 builder.append(parameter); 568 builder.append(' '); 569 builder.append(operator); 570 builder.append(' '); 571 builder.append(rvalue); 572 return builder.toString(); 573 } 574 575 public static String getPlainStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 576 Object rawValue = getRawValue(model, fieldDescriptor); 577 if (rawValue == null) { 578 return null; 579 } 580 String value = (String) rawValue; 581 if (value.equals("")) { 582 return null; 583 } 584 return value; 585 } 586 587 public static Integer getIntValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 588 Object rawValue = getRawValue(model, fieldDescriptor); 589 if (rawValue == null || "".equals(rawValue)) { 590 return null; 591 } else if (rawValue instanceof Integer) { 592 return (Integer) rawValue; 593 } else if (rawValue instanceof String) { 594 return Integer.valueOf((String) rawValue); 595 } else { 596 return Integer.valueOf(rawValue.toString()); 597 } 598 } 599 600 public static String getFieldType(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 601 String xpath = fieldDescriptor.getXpath(); 602 String schema = fieldDescriptor.getSchema(); 603 String name = fieldDescriptor.getName(); 604 try { 605 SchemaManager typeManager = Framework.getService(SchemaManager.class); 606 Field field = null; 607 if (xpath != null) { 608 if (model != null) { 609 field = model.getProperty(xpath).getField(); 610 } 611 } else { 612 if (schema != null) { 613 Schema schemaObj = typeManager.getSchema(schema); 614 if (schemaObj == null) { 615 throw new NuxeoException("failed to obtain schema: " + schema); 616 } 617 field = schemaObj.getField(name); 618 } else { 619 // assume named parameter use case: hard-code on String in this case 620 return StringType.ID; 621 } 622 } 623 if (field == null) { 624 throw new NuxeoException("failed to obtain field: " + schema + ":" + name); 625 } 626 Type type = field.getType(); 627 if (type instanceof SimpleTypeImpl) { 628 // type with constraint 629 type = type.getSuperType(); 630 } 631 return type.getName(); 632 } catch (PropertyException e) { 633 e.addInfo("failed to get field type for " + (xpath != null ? xpath : (schema + ":" + name))); 634 throw e; 635 } 636 } 637 638 @SuppressWarnings("unchecked") 639 public static Object getRawValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 640 String xpath = fieldDescriptor.getXpath(); 641 String schema = fieldDescriptor.getSchema(); 642 String name = fieldDescriptor.getName(); 643 try { 644 if (xpath != null) { 645 return model.getPropertyValue(xpath); 646 } else if (schema == null) { 647 return model.getPropertyValue(name); 648 } else { 649 return model.getProperty(schema, name); 650 } 651 } catch (PropertyNotFoundException e) { 652 // fall back on named parameters if any 653 Map<String, Object> params = (Map<String, Object>) model.getContextData( 654 PageProviderService.NAMED_PARAMETERS); 655 if (params != null) { 656 if (xpath != null) { 657 return params.get(xpath); 658 } else { 659 return params.get(name); 660 } 661 } 662 } catch (PropertyException e) { 663 return null; 664 } 665 return null; 666 } 667 668 public static String getStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 669 Object rawValue = getRawValue(model, fieldDescriptor); 670 if (rawValue == null) { 671 return null; 672 } 673 String value; 674 if (rawValue instanceof GregorianCalendar) { 675 GregorianCalendar gc = (GregorianCalendar) rawValue; 676 value = "TIMESTAMP '" + getDateFormat().format(gc.getTime()) + "'"; 677 } else if (rawValue instanceof Date) { 678 Date date = (Date) rawValue; 679 value = "TIMESTAMP '" + getDateFormat().format(date) + "'"; 680 } else if (rawValue instanceof Integer || rawValue instanceof Long || rawValue instanceof Double) { 681 value = rawValue.toString(); // no quotes 682 } else if (rawValue instanceof Boolean) { 683 value = ((Boolean) rawValue).booleanValue() ? "1" : "0"; 684 } else { 685 value = rawValue.toString().trim(); 686 if (value.equals("")) { 687 return null; 688 } 689 String fieldType = getFieldType(model, fieldDescriptor); 690 if ("long".equals(fieldType) || "integer".equals(fieldType) || "double".equals(fieldType)) { 691 return value; 692 } else { 693 return NXQL.escapeString(value); 694 } 695 } 696 return value; 697 } 698 699 protected static DateFormat getDateFormat() { 700 // not thread-safe so don't use a static instance 701 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 702 } 703 704 @SuppressWarnings("unchecked") 705 public static List<String> getListValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 706 Object rawValue = getRawValue(model, fieldDescriptor); 707 if (rawValue == null) { 708 return null; 709 } 710 List<String> values = new ArrayList<>(); 711 if (rawValue instanceof ArrayList) { 712 rawValue = ((ArrayList<Object>) rawValue).toArray(); 713 } 714 for (Object element : (Object[]) rawValue) { 715 if (element != null) { 716 if (element instanceof Number) { 717 values.add(element.toString()); 718 } else { 719 String value = element.toString().trim(); 720 if (!value.equals("")) { 721 values.add(NXQL.escapeString(value)); 722 } 723 } 724 } 725 } 726 return values; 727 } 728 729 public static Boolean getBooleanValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) { 730 Object rawValue = getRawValue(model, fieldDescriptor); 731 if (rawValue == null) { 732 return null; 733 } else { 734 return (Boolean) rawValue; 735 } 736 } 737 738 /** 739 * @since 8.4 740 */ 741 public static String appendClause(String query, String clause) { 742 if (StringUtils.isBlank(query)) { 743 return clause; 744 } else if (StringUtils.isBlank(clause)) { 745 return query; 746 } else { 747 return query + " AND " + clause; 748 } 749 } 750 751 /** 752 * @since 8.4 753 */ 754 public static String buildPattern(String pattern, String key, String replacement) { 755 int index = pattern.indexOf(key); 756 while (index >= 0) { 757 // All keys not prefixed by a letter or a digit has to be replaced, because 758 // It could be part of a schema name 759 if (!Character.isLetterOrDigit(pattern.charAt(index - 1)) && (index + key.length() == pattern.length() 760 || !Character.isLetterOrDigit(pattern.charAt(index + key.length())))) { 761 pattern = pattern.substring(0, index) + pattern.substring(index).replaceFirst(key, replacement); 762 } 763 index = pattern.indexOf(key, index + 1); 764 } 765 return pattern; 766 } 767 768}