001/*
002 * (C) Copyright 2010-2017 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.lang.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.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")) {
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                value = escaper.escape(value);
399            }
400            return serializeUnary(parameter, operator, value);
401
402        } else if (operator.equals("BETWEEN")) {
403            String min = getStringValue(model, values[0]);
404            String max = getStringValue(model, values[1]);
405
406            if (min != null && max != null) {
407                StringBuilder builder = new StringBuilder();
408                builder.append(parameter);
409                builder.append(' ');
410                builder.append(operator);
411                builder.append(' ');
412                builder.append(min);
413                builder.append(" AND ");
414                builder.append(max);
415                return builder.toString();
416            } else if (max != null) {
417                return serializeUnary(parameter, "<=", max);
418            } else if (min != null) {
419                return serializeUnary(parameter, ">=", min);
420            } else {
421                // both min and max are not provided, ignore predicate
422                return "";
423            }
424        } else if (operator.equals("IN")) {
425            List<String> options = getListValue(model, values[0]);
426            if (options == null || options.isEmpty()) {
427                return "";
428            } else if (options.size() == 1) {
429                return serializeUnary(parameter, "=", options.get(0));
430            } else {
431                StringBuilder builder = new StringBuilder();
432                builder.append(parameter);
433                builder.append(" IN (");
434                for (int i = 0; i < options.size(); i++) {
435                    if (i != 0) {
436                        builder.append(", ");
437                    }
438                    builder.append(options.get(i));
439                }
440                builder.append(')');
441                return builder.toString();
442            }
443        } else if (operator.equals("STARTSWITH")) {
444            String fieldType = getFieldType(model, values[0]);
445            if (fieldType.equals("string")) {
446                String value = getStringValue(model, values[0]);
447                if (value == null) {
448                    return "";
449                } else {
450                    return serializeUnary(parameter, operator, value);
451                }
452            } else {
453                List<String> options = getListValue(model, values[0]);
454                if (options == null || options.isEmpty()) {
455                    return "";
456                } else if (options.size() == 1) {
457                    return serializeUnary(parameter, operator, options.get(0));
458                } else {
459                    StringBuilder builder = new StringBuilder();
460                    builder.append('(');
461                    for (int i = 0; i < options.size() - 1; i++) {
462                        builder.append(serializeUnary(parameter, operator, options.get(i)));
463                        builder.append(" OR ");
464                    }
465                    builder.append(serializeUnary(parameter, operator, options.get(options.size() - 1)));
466                    builder.append(')');
467                    return builder.toString();
468                }
469            }
470        } else if (operator.equals("EMPTY") || operator.equals("ISEMPTY")) {
471            return parameter + " = ''";
472        } else if (operator.equals("FULLTEXT ALL") // BBB
473                || operator.equals("FULLTEXT")) {
474            String value = getPlainStringValue(model, values[0]);
475            if (value == null) {
476                // value not provided: ignore predicate
477                return "";
478            }
479            if (escaper != null) {
480                value = escaper.escape(value);
481            }
482            return parameter + ' ' + serializeFullText(value);
483        } else if (operator.equals("IS NULL")) {
484            Boolean value = getBooleanValue(model, values[0]);
485            if (value == null) {
486                // value not provided: ignore predicate
487                return "";
488            } else if (Boolean.TRUE.equals(value)) {
489                return parameter + " IS NULL";
490            } else {
491                return parameter + " IS NOT NULL";
492            }
493        } else {
494            throw new NuxeoException("Unsupported operator: " + operator);
495        }
496    }
497
498    protected static String getParameterWithHint(String operator, String parameter, String hint) {
499        String ret = parameter;
500        // add ecm:fulltext. prefix if needed
501        if ((operator.equals("FULLTEXT ALL") || operator.equals("FULLTEXT"))
502                && !parameter.startsWith(NXQL.ECM_FULLTEXT)) {
503             ret = NXQL.ECM_FULLTEXT + '.' + parameter;
504        }
505        // add the hint
506        if (hint != null && !hint.isEmpty()) {
507            ret = String.format("/*+%s */ %s", hint.trim(), ret);
508        }
509        return ret;
510    }
511
512    public static final String DEFAULT_SPECIAL_CHARACTERS_REGEXP = "!#$%&'()+,./\\\\:-@{|}`^~";
513
514    public static final String IGNORED_CHARS_KEY = "org.nuxeo.query.builder.ignored.chars";
515
516    /**
517     * Remove any special character that could be mis-interpreted as a low level full-text query operator. This method
518     * should be used by user facing callers of CoreQuery*PageProviders that use a fixed part or a pattern query. Fields
519     * in where clause already dealt with.
520     *
521     * @since 5.6
522     * @return sanitized text value
523     */
524    public static String sanitizeFulltextInput(String value) {
525        // Ideally the low level full-text language
526        // parser should be robust to any user input however this is much more
527        // complicated to implement correctly than the following simple user
528        // input filtering scheme.
529        ConfigurationService cs = Framework.getService(ConfigurationService.class);
530        String ignoredChars = cs.getProperty(IGNORED_CHARS_KEY, DEFAULT_SPECIAL_CHARACTERS_REGEXP);
531        String res = "";
532        value = value.replaceAll("[" + ignoredChars + "]", " ");
533        value = value.trim();
534        String[] tokens = value.split("[\\s]+");
535        for (String token : tokens) {
536            if ("-".equals(token) || "*".equals(token) || "*-".equals(token) || "-*".equals(token)) {
537                continue;
538            }
539            if (res.length() > 0) {
540                res += " ";
541            }
542            if (token.startsWith("-") || token.endsWith("*")) {
543                res += token;
544            } else {
545                res += token.replace("-", " ").replace("*", " ");
546            }
547        }
548        return res.trim();
549    }
550
551    public static String serializeFullText(String value) {
552        value = sanitizeFulltextInput(value);
553        return "= " + NXQL.escapeString(value);
554    }
555
556    protected static String serializeUnary(String parameter, String operator, String rvalue) {
557        StringBuilder builder = new StringBuilder();
558        builder.append(parameter);
559        builder.append(' ');
560        builder.append(operator);
561        builder.append(' ');
562        builder.append(rvalue);
563        return builder.toString();
564    }
565
566    public static String getPlainStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
567        Object rawValue = getRawValue(model, fieldDescriptor);
568        if (rawValue == null) {
569            return null;
570        }
571        String value = (String) rawValue;
572        if (value.equals("")) {
573            return null;
574        }
575        return value;
576    }
577
578    public static Integer getIntValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
579        Object rawValue = getRawValue(model, fieldDescriptor);
580        if (rawValue == null || "".equals(rawValue)) {
581            return null;
582        } else if (rawValue instanceof Integer) {
583            return (Integer) rawValue;
584        } else if (rawValue instanceof String) {
585            return Integer.valueOf((String) rawValue);
586        } else {
587            return Integer.valueOf(rawValue.toString());
588        }
589    }
590
591    public static String getFieldType(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
592        String xpath = fieldDescriptor.getXpath();
593        String schema = fieldDescriptor.getSchema();
594        String name = fieldDescriptor.getName();
595        try {
596            SchemaManager typeManager = Framework.getService(SchemaManager.class);
597            Field field = null;
598            if (xpath != null) {
599                if (model != null) {
600                    field = model.getProperty(xpath).getField();
601                }
602            } else {
603                if (schema != null) {
604                    Schema schemaObj = typeManager.getSchema(schema);
605                    if (schemaObj == null) {
606                        throw new NuxeoException("failed to obtain schema: " + schema);
607                    }
608                    field = schemaObj.getField(name);
609                } else {
610                    // assume named parameter use case: hard-code on String in this case
611                    return StringType.ID;
612                }
613            }
614            if (field == null) {
615                throw new NuxeoException("failed to obtain field: " + schema + ":" + name);
616            }
617            Type type = field.getType();
618            if (type instanceof SimpleTypeImpl) {
619                // type with constraint
620                type = type.getSuperType();
621            }
622            return type.getName();
623        } catch (PropertyException e) {
624            e.addInfo("failed to get field type for " + (xpath != null ? xpath : (schema + ":" + name)));
625            throw e;
626        }
627    }
628
629    @SuppressWarnings("unchecked")
630    public static Object getRawValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
631        String xpath = fieldDescriptor.getXpath();
632        String schema = fieldDescriptor.getSchema();
633        String name = fieldDescriptor.getName();
634        try {
635            if (xpath != null) {
636                return model.getPropertyValue(xpath);
637            } else if (schema == null) {
638                return model.getPropertyValue(name);
639            } else {
640                return model.getProperty(schema, name);
641            }
642        } catch (PropertyNotFoundException e) {
643            // fall back on named parameters if any
644            Map<String, Object> params = (Map<String, Object>) model.getContextData(
645                    PageProviderService.NAMED_PARAMETERS);
646            if (params != null) {
647                if (xpath != null) {
648                    return params.get(xpath);
649                } else {
650                    return params.get(name);
651                }
652            }
653        } catch (PropertyException e) {
654            return null;
655        }
656        return null;
657    }
658
659    public static String getStringValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
660        Object rawValue = getRawValue(model, fieldDescriptor);
661        if (rawValue == null) {
662            return null;
663        }
664        String value;
665        if (rawValue instanceof GregorianCalendar) {
666            GregorianCalendar gc = (GregorianCalendar) rawValue;
667            value = "TIMESTAMP '" + getDateFormat().format(gc.getTime()) + "'";
668        } else if (rawValue instanceof Date) {
669            Date date = (Date) rawValue;
670            value = "TIMESTAMP '" + getDateFormat().format(date) + "'";
671        } else if (rawValue instanceof Integer || rawValue instanceof Long || rawValue instanceof Double) {
672            value = rawValue.toString(); // no quotes
673        } else if (rawValue instanceof Boolean) {
674            value = ((Boolean) rawValue).booleanValue() ? "1" : "0";
675        } else {
676            value = rawValue.toString().trim();
677            if (value.equals("")) {
678                return null;
679            }
680            String fieldType = getFieldType(model, fieldDescriptor);
681            if ("long".equals(fieldType) || "integer".equals(fieldType) || "double".equals(fieldType)) {
682                return value;
683            } else {
684                return NXQL.escapeString(value);
685            }
686        }
687        return value;
688    }
689
690    protected static DateFormat getDateFormat() {
691        // not thread-safe so don't use a static instance
692        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
693    }
694
695    @SuppressWarnings("unchecked")
696    public static List<String> getListValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
697        Object rawValue = getRawValue(model, fieldDescriptor);
698        if (rawValue == null) {
699            return null;
700        }
701        List<String> values = new ArrayList<>();
702        if (rawValue instanceof ArrayList) {
703            rawValue = ((ArrayList<Object>) rawValue).toArray();
704        }
705        for (Object element : (Object[]) rawValue) {
706            if (element != null) {
707                if (element instanceof Number) {
708                    values.add(element.toString());
709                } else {
710                    String value = element.toString().trim();
711                    if (!value.equals("")) {
712                        values.add(NXQL.escapeString(value));
713                    }
714                }
715            }
716        }
717        return values;
718    }
719
720    public static Boolean getBooleanValue(DocumentModel model, PredicateFieldDefinition fieldDescriptor) {
721        Object rawValue = getRawValue(model, fieldDescriptor);
722        if (rawValue == null) {
723            return null;
724        } else {
725            return (Boolean) rawValue;
726        }
727    }
728
729    /**
730     * @since 8.4
731     */
732    public static String appendClause(String query, String clause) {
733        return query + " AND " + clause;
734    }
735
736    /**
737     * @since 8.4
738     */
739    public static String buildPattern(String pattern, String key, String replacement) {
740        int index = pattern.indexOf(key);
741        while (index >= 0) {
742            // All keys not prefixed by a letter or a digit has to be replaced, because
743            // It could be part of a schema name
744            if (!Character.isLetterOrDigit(pattern.charAt(index - 1)) && (index + key.length() == pattern.length()
745                    || !Character.isLetterOrDigit(pattern.charAt(index + key.length())))) {
746                pattern = pattern.substring(0, index) + pattern.substring(index).replaceFirst(key, replacement);
747            }
748            index = pattern.indexOf(key, index + 1);
749        }
750        return pattern;
751    }
752
753}