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