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.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.core.api.PropertyException;
038import org.nuxeo.ecm.core.api.SortInfo;
039import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
040import org.nuxeo.ecm.core.query.sql.NXQL;
041import org.nuxeo.ecm.core.query.sql.model.Literal;
042import org.nuxeo.ecm.core.schema.SchemaManager;
043import org.nuxeo.ecm.core.schema.types.Field;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl;
046import org.nuxeo.ecm.core.schema.types.Type;
047import org.nuxeo.ecm.core.schema.types.primitives.StringType;
048import org.nuxeo.ecm.core.search.api.client.querymodel.Escaper;
049import org.nuxeo.ecm.platform.query.api.PageProviderService;
050import org.nuxeo.ecm.platform.query.api.PredicateDefinition;
051import org.nuxeo.ecm.platform.query.api.PredicateFieldDefinition;
052import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition;
053import org.nuxeo.ecm.platform.query.core.FieldDescriptor;
054import org.nuxeo.runtime.api.Framework;
055import org.nuxeo.runtime.services.config.ConfigurationService;
056
057/**
058 * Helper to generate NXQL queries from XMap descriptors
059 *
060 * @since 5.4
061 * @author Anahide Tchertchian
062 */
063public class NXQLQueryBuilder {
064
065    private static final Log log = LogFactory.getLog(NXQLQueryBuilder.class);
066
067    // @since 5.9.2
068    public static final String DEFAULT_SELECT_STATEMENT = "SELECT * FROM Document";
069
070    // @since 5.9
071    public static final String SORTED_COLUMN = "SORTED_COLUMN";
072
073    public static final String REGEXP_NAMED_PARAMETER = "[^a-zA-Z]:\\s*" + "([a-zA-Z0-9:]*)";
074
075    public static final String REGEXP_EXCLUDE_QUOTE = "'[^']*'";
076
077    public static final String REGEXP_EXCLUDE_DOUBLE_QUOTE = "\"[^\"]*\"";
078
079    private NXQLQueryBuilder() {
080    }
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        if (whereClause != null) {
117            queryBuilder.append(getQueryElement(model, whereClause, quickFiltersClause, params));
118        }
119        String sortClause = getSortClause(sortInfos);
120        if (sortClause != null && sortClause.length() > 0) {
121            queryBuilder.append(" ");
122            queryBuilder.append(sortClause);
123        }
124        return queryBuilder.toString().trim();
125    }
126
127    public static String getQueryElement(DocumentModel model, WhereClauseDefinition whereClause, Object[] params) {
128        return getQueryElement(model, whereClause, null, params);
129    }
130
131    /**
132     * @since 8.4
133     */
134    public static String getQueryElement(DocumentModel model, WhereClauseDefinition whereClause,
135            String quickFiltersClause, Object[] params) {
136        List<String> elements = new ArrayList<String>();
137        PredicateDefinition[] predicates = whereClause.getPredicates();
138        if (predicates != null) {
139            Escaper escaper = null;
140            Class<? extends Escaper> escaperClass = whereClause.getEscaperClass();
141            if (escaperClass != null) {
142                try {
143                    escaper = escaperClass.newInstance();
144                } catch (ReflectiveOperationException e) {
145                    throw new NuxeoException(e);
146                }
147            }
148            for (PredicateDefinition predicate : predicates) {
149                String predicateString = getQueryElement(model, predicate, escaper);
150                if (predicateString == null) {
151                    continue;
152                }
153
154                predicateString = predicateString.trim();
155                if (!predicateString.equals("")) {
156                    elements.add(predicateString);
157                }
158            }
159        }
160        // add fixed part if applicable
161        String fixedPart = whereClause.getFixedPart();
162        if (!StringUtils.isBlank(fixedPart)) {
163            if (StringUtils.isNotBlank(quickFiltersClause)) {
164                fixedPart = appendClause(fixedPart, quickFiltersClause);
165            }
166            if (elements.isEmpty()) {
167                elements.add(getQuery(fixedPart, params, whereClause.getQuoteFixedPartParameters(),
168                        whereClause.getEscapeFixedPartParameters(), model));
169            } else {
170                elements.add('(' + getQuery(fixedPart, params, whereClause.getQuoteFixedPartParameters(),
171                        whereClause.getEscapeFixedPartParameters(), model) + ')');
172            }
173        } else if (StringUtils.isNotBlank(quickFiltersClause)) {
174            fixedPart = quickFiltersClause;
175        }
176
177        if (elements.isEmpty()) {
178            return "";
179        }
180
181        // XXX: for now only a one level implement conjunctive WHERE clause
182        String clauseValues = StringUtils.join(elements, " AND ").trim();
183
184        // GR: WHERE (x = 1) is invalid NXQL
185        while (elements.size() == 1 && clauseValues.startsWith("(") && clauseValues.endsWith(")")) {
186            clauseValues = clauseValues.substring(1, clauseValues.length() - 1).trim();
187        }
188        if (clauseValues.length() == 0) {
189            return "";
190        }
191        return " WHERE " + clauseValues;
192    }
193
194    public static String getQuery(String pattern, Object[] params, boolean quoteParameters, boolean escape,
195            DocumentModel searchDocumentModel, SortInfo... sortInfos) {
196        String sortedColumn;
197        if (sortInfos == null || sortInfos.length == 0) {
198            // If there is no ORDER BY use the id
199            sortedColumn = NXQL.ECM_UUID;
200        } else {
201            sortedColumn = sortInfos[0].getSortColumn();
202        }
203        if (pattern != null && pattern.contains(SORTED_COLUMN)) {
204            pattern = pattern.replace(SORTED_COLUMN, sortedColumn);
205        }
206        StringBuilder queryBuilder;
207
208        // handle named parameters replacements
209        if (searchDocumentModel != null) {
210            // Find all query named parameters as ":parameter" not between
211            // quotes and add them to matches
212            String query = pattern.replaceAll(REGEXP_EXCLUDE_DOUBLE_QUOTE, StringUtils.EMPTY);
213            query = query.replaceAll(REGEXP_EXCLUDE_QUOTE, StringUtils.EMPTY);
214            Pattern p1 = Pattern.compile(REGEXP_NAMED_PARAMETER);
215            Matcher m1 = p1.matcher(query);
216            List<String> matches = new ArrayList<String>();
217            while (m1.find()) {
218                matches.add(m1.group().substring(m1.group().indexOf(":") + 1));
219            }
220            for (String key : matches) {
221                Object parameter = getRawValue(searchDocumentModel, new FieldDescriptor(key));
222                if (parameter == null) {
223                    continue;
224                }
225                key = ":" + key;
226                if (parameter instanceof String[]) {
227                    replaceStringList(pattern, Arrays.asList((String[]) parameter), quoteParameters, escape, key);
228                } else if (parameter instanceof List) {
229                    replaceStringList(pattern, (List<?>) parameter, quoteParameters, escape, key);
230                } else if (parameter instanceof Boolean) {
231                    pattern = buildPattern(pattern, key, ((Boolean) parameter) ? "1" : "0");
232                } else if (parameter instanceof Number) {
233                    pattern = buildPattern(pattern, key, parameter.toString());
234                } else if (parameter instanceof Literal) {
235                    if (quoteParameters) {
236                        pattern = buildPattern(pattern, key, "'" + parameter.toString() + "'");
237                    } else {
238                        pattern = buildPattern(pattern, key, ((Literal) parameter).asString());
239                    }
240                } else {
241                    if (quoteParameters) {
242                        pattern = buildPattern(pattern, key, "'" + parameter + "'");
243                    } else {
244                        pattern = buildPattern(pattern, key, parameter != null ? parameter.toString() : null);
245                    }
246                }
247            }
248        }
249
250        if (params == null) {
251            queryBuilder = new StringBuilder(pattern + ' ');
252        } else {
253            // handle "standard" parameters replacements (referenced by ? characters)
254            // XXX: the + " " is a workaround for the buggy implementation
255            // of the split function in case the pattern ends with '?'
256            String[] queryStrList = (pattern + ' ').split("\\?");
257            queryBuilder = new StringBuilder(queryStrList[0]);
258            for (int i = 0; i < params.length; i++) {
259                if (params[i] instanceof String[]) {
260                    appendStringList(queryBuilder, Arrays.asList((String[]) params[i]), quoteParameters, escape);
261                } else if (params[i] instanceof List) {
262                    appendStringList(queryBuilder, (List<?>) params[i], quoteParameters, escape);
263                } else if (params[i] instanceof Boolean) {
264                    boolean b = ((Boolean) params[i]).booleanValue();
265                    queryBuilder.append(b ? 1 : 0);
266                } else if (params[i] instanceof Number) {
267                    queryBuilder.append(params[i]);
268                } else if (params[i] instanceof Literal) {
269                    if (quoteParameters) {
270                        queryBuilder.append(params[i].toString());
271                    } else {
272                        queryBuilder.append(((Literal) params[i]).asString());
273                    }
274                } else {
275                    if (params[i] == null) {
276                        if (quoteParameters) {
277                            queryBuilder.append("''");
278                        }
279                    } else {
280                        String queryParam = params[i].toString();
281                        queryBuilder.append(prepareStringLiteral(queryParam, quoteParameters, escape));
282                    }
283                }
284                queryBuilder.append(queryStrList[i + 1]);
285            }
286        }
287        queryBuilder.append(getSortClause(sortInfos));
288        return queryBuilder.toString().trim();
289    }
290
291    public static void appendStringList(StringBuilder queryBuilder, List<?> listParam, boolean quoteParameters,
292            boolean escape) {
293        // avoid appending parentheses if the query builder ends with one
294        boolean addParentheses = !queryBuilder.toString().endsWith("(");
295        if (addParentheses) {
296            queryBuilder.append('(');
297        }
298        List<String> result = new ArrayList<String>(listParam.size());
299        for (Object param : listParam) {
300            result.add(prepareStringLiteral(param.toString(), quoteParameters, escape));
301        }
302        queryBuilder.append(StringUtils.join(result, ", "));
303        if (addParentheses) {
304            queryBuilder.append(')');
305        }
306    }
307
308    public static String replaceStringList(String pattern, List<?> listParams, boolean quoteParameters, boolean escape,
309            String key) {
310        List<String> result = new ArrayList<String>(listParams.size());
311        for (Object param : listParams) {
312            result.add(prepareStringLiteral(param.toString(), quoteParameters, escape));
313        }
314
315        return buildPattern(pattern, key, '(' + StringUtils.join(result, ", " + "") + ')');
316    }
317
318    /**
319     * Return the string literal in a form ready to embed in an NXQL statement.
320     */
321    public static String prepareStringLiteral(String s, boolean quoteParameter, boolean escape) {
322        if (escape) {
323            if (quoteParameter) {
324                return NXQL.escapeString(s);
325            } else {
326                return NXQL.escapeStringInner(s);
327            }
328        } else {
329            if (quoteParameter) {
330                return "'" + s + "'";
331            } else {
332                return s;
333            }
334        }
335    }
336
337    public static String getQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor, Escaper escaper) {
338        String type = predicateDescriptor.getType();
339        if (PredicateDefinition.ATOMIC_PREDICATE.equals(type)) {
340            return atomicQueryElement(model, predicateDescriptor, escaper);
341        }
342        if (PredicateDefinition.SUB_CLAUSE_PREDICATE.equals(type)) {
343            return subClauseQueryElement(model, predicateDescriptor);
344        }
345        throw new NuxeoException("Unknown predicate type: " + type);
346    }
347
348    protected static String subClauseQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor) {
349        PredicateFieldDefinition[] values = predicateDescriptor.getValues();
350        if (values == null || values.length != 1) {
351            throw new NuxeoException("subClause predicate needs exactly one field");
352        }
353        PredicateFieldDefinition fieldDescriptor = values[0];
354        if (!getFieldType(model, fieldDescriptor).equals("string")) {
355            if (fieldDescriptor.getXpath() != null) {
356                throw new NuxeoException(String.format("type of field %s is not string", fieldDescriptor.getXpath()));
357            } else {
358                throw new NuxeoException(String.format("type of field %s.%s is not string",
359                        fieldDescriptor.getSchema(), fieldDescriptor.getName()));
360            }
361        }
362        Object subclauseValue = getRawValue(model, fieldDescriptor);
363        if (subclauseValue == null) {
364            return "";
365        }
366
367        return "(" + subclauseValue + ")";
368    }
369
370    protected static String atomicQueryElement(DocumentModel model, PredicateDefinition predicateDescriptor,
371            Escaper escaper) {
372        String operator = null;
373        String operatorField = predicateDescriptor.getOperatorField();
374        String operatorSchema = predicateDescriptor.getOperatorSchema();
375        PredicateFieldDefinition[] values = predicateDescriptor.getValues();
376        if (operatorField != null && operatorSchema != null) {
377            PredicateFieldDefinition operatorFieldDescriptor = new FieldDescriptor(operatorSchema, operatorField);
378            operator = getPlainStringValue(model, operatorFieldDescriptor);
379            if (operator != null) {
380                operator = operator.toUpperCase();
381            }
382        }
383        if (StringUtils.isBlank(operator)) {
384            operator = predicateDescriptor.getOperator();
385        }
386        String hint = predicateDescriptor.getHint();
387        String parameter = getParameterWithHint(operator, predicateDescriptor.getParameter(), hint);
388
389        if (operator.equals("=") || operator.equals("!=") || operator.equals("<") || operator.equals(">")
390                || operator.equals("<=") || operator.equals(">=") || operator.equals("<>") || operator.equals("LIKE")
391                || operator.equals("ILIKE")) {
392            // Unary predicate
393            String value = getStringValue(model, values[0]);
394            if (value == null) {
395                // value not provided: ignore predicate
396                return "";
397            }
398            if (escaper != null && (operator.equals("LIKE") || operator.equals("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")) {
426            List<String> options = getListValue(model, values[0]);
427            if (options == null || options.isEmpty()) {
428                return "";
429            } else if (options.size() == 1) {
430                return serializeUnary(parameter, "=", options.get(0));
431            } else {
432                StringBuilder builder = new StringBuilder();
433                builder.append(parameter);
434                builder.append(" IN (");
435                for (int i = 0; i < options.size(); i++) {
436                    if (i != 0) {
437                        builder.append(", ");
438                    }
439                    builder.append(options.get(i));
440                }
441                builder.append(')');
442                return builder.toString();
443            }
444        } else if (operator.equals("STARTSWITH")) {
445            String fieldType = getFieldType(model, values[0]);
446            if (fieldType.equals("string")) {
447                String value = getStringValue(model, values[0]);
448                if (value == null) {
449                    return "";
450                } else {
451                    return serializeUnary(parameter, operator, value);
452                }
453            } else {
454                List<String> options = getListValue(model, values[0]);
455                if (options == null || options.isEmpty()) {
456                    return "";
457                } else if (options.size() == 1) {
458                    return serializeUnary(parameter, operator, options.get(0));
459                } else {
460                    StringBuilder builder = new StringBuilder();
461                    builder.append('(');
462                    for (int i = 0; i < options.size() - 1; i++) {
463                        builder.append(serializeUnary(parameter, operator, options.get(i)));
464                        builder.append(" OR ");
465                    }
466                    builder.append(serializeUnary(parameter, operator, options.get(options.size() - 1)));
467                    builder.append(')');
468                    return builder.toString();
469                }
470            }
471        } else if (operator.equals("EMPTY") || operator.equals("ISEMPTY")) {
472            return parameter + " = ''";
473        } else if (operator.equals("FULLTEXT ALL") // BBB
474                || operator.equals("FULLTEXT")) {
475            String value = getPlainStringValue(model, values[0]);
476            if (value == null) {
477                // value not provided: ignore predicate
478                return "";
479            }
480            if (escaper != null) {
481                value = escaper.escape(value);
482            }
483            return parameter + ' ' + serializeFullText(value);
484        } else if (operator.equals("IS NULL")) {
485            Boolean value = getBooleanValue(model, values[0]);
486            if (value == null) {
487                // value not provided: ignore predicate
488                return "";
489            } else if (Boolean.TRUE.equals(value)) {
490                return parameter + " IS NULL";
491            } else {
492                return parameter + " IS NOT NULL";
493            }
494        } else {
495            throw new NuxeoException("Unsupported operator: " + operator);
496        }
497    }
498
499    protected static String getParameterWithHint(String operator, String parameter, String hint) {
500        String ret = parameter;
501        // add ecm:fulltext. prefix if needed
502        if ((operator.equals("FULLTEXT ALL") || operator.equals("FULLTEXT"))
503                && !parameter.startsWith(NXQL.ECM_FULLTEXT)) {
504             ret = NXQL.ECM_FULLTEXT + '.' + parameter;
505        }
506        // add the hint
507        if (hint != null && !hint.isEmpty()) {
508            ret = String.format("/*+%s */ %s", hint.trim(), ret);
509        }
510        return ret;
511    }
512
513    /**
514     * Prepares a statement for a fulltext field by converting FULLTEXT virtual operators to a syntax that the search
515     * syntax accepts.
516     *
517     * @param value
518     * @return the serialized statement
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.getProperty(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 (int i = 0; i < tokens.length; i++) {
545            if ("-".equals(tokens[i]) || "*".equals(tokens[i]) || "*-".equals(tokens[i]) || "-*".equals(tokens[i])) {
546                continue;
547            }
548            if (res.length() > 0) {
549                res += " ";
550            }
551            if (tokens[i].startsWith("-") || tokens[i].endsWith("*")) {
552                res += tokens[i];
553            } else {
554                res += tokens[i].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<String>();
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        return query + " AND " + clause;
743    }
744
745    /**
746     * @since 8.4
747     */
748    public static String buildPattern(String pattern, String key, String replacement) {
749        int index = pattern.indexOf(key);
750        while (index >= 0) {
751            // All keys not prefixed by a letter or a digit has to be replaced, because
752            // It could be part of a schema name
753            if (!Character.isLetterOrDigit(pattern.charAt(index - 1)) && (index + key.length() == pattern.length()
754                    || !Character.isLetterOrDigit(pattern.charAt(index + key.length())))) {
755                pattern = pattern.substring(0, index) + pattern.substring(index).replaceFirst(key, replacement);
756            }
757            index = pattern.indexOf(key, index + 1);
758        }
759        return pattern;
760    }
761
762}