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