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