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}