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