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