001/*
002 * (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.dbs;
020
021import static java.lang.Boolean.FALSE;
022import static java.lang.Boolean.TRUE;
023import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.FACETED_TAG;
024import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.FACETED_TAG_LABEL;
025import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL;
026import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL_NAME;
027import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP;
028import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID;
029import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES;
030import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME;
031import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID;
032import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE;
033import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL;
034import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.PROP_MAJOR_VERSION;
035import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.PROP_MINOR_VERSION;
036
037import java.io.Serializable;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.Calendar;
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.HashSet;
044import java.util.Iterator;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048
049import org.apache.commons.lang.StringUtils;
050import org.apache.commons.lang.math.NumberUtils;
051import org.apache.commons.logging.Log;
052import org.apache.commons.logging.LogFactory;
053import org.nuxeo.ecm.core.query.QueryParseException;
054import org.nuxeo.ecm.core.query.sql.NXQL;
055import org.nuxeo.ecm.core.query.sql.model.Expression;
056import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
057import org.nuxeo.ecm.core.query.sql.model.Operand;
058import org.nuxeo.ecm.core.query.sql.model.OrderByClause;
059import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
060import org.nuxeo.ecm.core.query.sql.model.Reference;
061import org.nuxeo.ecm.core.query.sql.model.SQLQuery;
062import org.nuxeo.ecm.core.query.sql.model.SelectClause;
063import org.nuxeo.ecm.core.query.sql.model.SelectList;
064import org.nuxeo.ecm.core.schema.DocumentType;
065import org.nuxeo.ecm.core.schema.SchemaManager;
066import org.nuxeo.ecm.core.schema.types.ComplexType;
067import org.nuxeo.ecm.core.schema.types.Field;
068import org.nuxeo.ecm.core.schema.types.ListType;
069import org.nuxeo.ecm.core.schema.types.Schema;
070import org.nuxeo.ecm.core.schema.types.Type;
071import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
072import org.nuxeo.ecm.core.schema.types.primitives.DateType;
073import org.nuxeo.ecm.core.storage.ExpressionEvaluator;
074import org.nuxeo.ecm.core.storage.State;
075import org.nuxeo.runtime.api.Framework;
076
077/**
078 * Expression evaluator for a {@link DBSDocument} state.
079 *
080 * @since 5.9.4
081 */
082public class DBSExpressionEvaluator extends ExpressionEvaluator {
083
084    private static final Log log = LogFactory.getLog(DBSExpressionEvaluator.class);
085
086    private static final Long ZERO = Long.valueOf(0);
087
088    private static final Long ONE = Long.valueOf(1);
089
090    protected final SelectClause selectClause;
091
092    protected final Expression expression;
093
094    protected final OrderByClause orderByClause;
095
096    protected SchemaManager schemaManager;
097
098    protected List<String> documentTypes;
099
100    protected State state;
101
102    protected boolean parsing;
103
104    /** Info about a value and how to compute it from the toplevel state or an iterator's state. */
105    protected static final class ValueInfo {
106
107        /**
108         * Traversed steps to compute this value from a state. Traversal steps can be:
109         * <ul>
110         * <li>String: a map key.
111         * <li>Integer: a list element.
112         * </ul>
113         */
114        // also used to temporarily hold the full parsed reference
115        public List<Serializable> steps;
116
117        // original NXQL name in query
118        public final String nxqlProp;
119
120        public final String canonRef;
121
122        public Type type;
123
124        public boolean isTrueOrNullBoolean;
125
126        public boolean isDateCast;
127
128        /** The value computed for this reference. */
129        public Object value;
130
131        public ValueInfo(List<Serializable> steps, String nxqlProp, String canonRef) {
132            this.steps = steps;
133            this.nxqlProp = nxqlProp;
134            this.canonRef = canonRef;
135        }
136
137        public Object getValueForEvaluation() {
138            if (type instanceof BooleanType) {
139                // boolean evaluation is like 0 / 1
140                if (isTrueOrNullBoolean) {
141                    return TRUE.equals(value) ? ONE : ZERO;
142                } else {
143                    return value == null ? null : (((Boolean) value).booleanValue() ? ONE : ZERO);
144                }
145            } else if (isDateCast) {
146                if (value == null) {
147                    return null;
148                } else if (value instanceof Calendar) {
149                    return castToDate((Calendar) value);
150                } else { // array
151                    Object[] array = (Object[]) value;
152                    List<Calendar> dates = new ArrayList<>(array.length);
153                    for (Object v : array) {
154                        v = v instanceof Calendar ? castToDate((Calendar) v) : null;
155                        dates.add((Calendar) v);
156                    }
157                    return dates.toArray();
158                }
159            } else if (value == null && type instanceof ListType && ((ListType) type).isArray()) {
160                // don't use null, as list-based matches don't use ternary logic
161                return new Object[0];
162            } else {
163                return value;
164            }
165        }
166
167        protected Calendar castToDate(Calendar date) {
168            date.set(Calendar.HOUR_OF_DAY, 0);
169            date.set(Calendar.MINUTE, 0);
170            date.set(Calendar.SECOND, 0);
171            date.set(Calendar.MILLISECOND, 0);
172            return date;
173        }
174
175        @Override
176        public String toString() {
177            return "ValueInfo(" + canonRef + " " + steps + " = " + value + ")";
178        }
179    }
180
181    /**
182     * Info about an iterator and how to compute it from a state.
183     * <p>
184     * The iterator iterates over a list of states or scalars and can be reset to a new list.
185     * <p>
186     * Also contains information about dependent values and iterators.
187     */
188    protected static final class IterInfo implements Iterator<Object> {
189
190        /**
191         * Traversed steps to compute this iterator list from a state. Traversal steps can be:
192         * <ul>
193         * <li>String: a map key.
194         * <li>Integer: a list element.
195         * </ul>
196         */
197        public final List<Serializable> steps;
198
199        public final List<ValueInfo> dependentValueInfos = new ArrayList<>(2);
200
201        public final List<IterInfo> dependentIterInfos = new ArrayList<>(2);
202
203        protected List<Object> list;
204
205        protected Iterator<Object> it;
206
207        public IterInfo(List<Serializable> steps) {
208            this.steps = steps;
209        }
210
211        public void setList(Object list) {
212            if (list == null) {
213                this.list = Collections.emptyList();
214            } else if (list instanceof List) {
215                @SuppressWarnings("unchecked")
216                List<Object> stateList = (List<Object>) list;
217                this.list = stateList;
218            } else {
219                this.list = Arrays.asList((Object[]) list);
220            }
221            reset();
222        }
223
224        public void reset() {
225            it = list.iterator();
226        }
227
228        @Override
229        public boolean hasNext() {
230            return it.hasNext();
231        }
232
233        @Override
234        public Object next() {
235            return it.next();
236        }
237
238        @Override
239        public String toString() {
240            return "IterInfo(" + System.identityHashCode(this) + "," + steps + ")";
241        }
242    }
243
244    protected static class DBSPathResolver implements PathResolver {
245        protected final DBSSession session;
246
247        public DBSPathResolver(DBSSession session) {
248            this.session = session;
249        }
250
251        @Override
252        public String getIdForPath(String path) {
253            return session.getDocumentIdByPath(path);
254        }
255    }
256
257    /** For each encountered reference in traversal order, the corresponding value info. */
258    protected List<ValueInfo> referenceValueInfos;
259
260    /** Map of canonical reference to value info. */
261    protected Map<String, ValueInfo> canonicalReferenceValueInfos;
262
263    /** Map of canonical reference prefix to iterator. */
264    protected Map<String, IterInfo> canonicalPrefixIterInfos;
265
266    /** List of all iterators, in reversed order. */
267    protected List<IterInfo> allIterInfos;
268
269    /** The toplevel iterators. */
270    protected List<IterInfo> toplevelIterInfos;
271
272    /** The toplevel values, computed without wildcards. */
273    protected List<ValueInfo> toplevelValueInfos;
274
275    // correlation to use for each uncorrelated wildcard (negative to avoid collisions with correlated ones)
276    protected int uncorrelatedCounter;
277
278    // did we find a wildcard in the SELECT projection or WHERE expression
279    protected boolean hasWildcard;
280
281    // which reference index is being visited, reset / updated during each pass
282    protected int refCount;
283
284    public DBSExpressionEvaluator(DBSSession session, SQLQuery query, String[] principals,
285            boolean fulltextSearchDisabled) {
286        super(new DBSPathResolver(session), principals, fulltextSearchDisabled);
287        this.selectClause = query.select;
288        this.expression = query.where.predicate;
289        this.orderByClause = query.orderBy;
290    }
291
292    public SelectClause getSelectClause() {
293        return selectClause;
294    }
295
296    public Expression getExpression() {
297        return expression;
298    }
299
300    public OrderByClause getOrderByClause() {
301        return orderByClause;
302    }
303
304    protected List<String> getDocumentTypes() {
305        // TODO precompute in SchemaManager
306        if (documentTypes == null) {
307            documentTypes = new ArrayList<>();
308            for (DocumentType docType : schemaManager.getDocumentTypes()) {
309                documentTypes.add(docType.getName());
310            }
311        }
312        return documentTypes;
313    }
314
315    protected Set<String> getMixinDocumentTypes(String mixin) {
316        Set<String> types = schemaManager.getDocumentTypeNamesForFacet(mixin);
317        return types == null ? Collections.emptySet() : types;
318    }
319
320    protected boolean isNeverPerInstanceMixin(String mixin) {
321        return schemaManager.getNoPerDocumentQueryFacets().contains(mixin);
322    }
323
324    /**
325     * Initializes parsing datastructures.
326     */
327    public void parse() {
328        schemaManager = Framework.getService(SchemaManager.class);
329
330        referenceValueInfos = new ArrayList<>();
331        canonicalReferenceValueInfos = new HashMap<>();
332        allIterInfos = new ArrayList<>();
333        toplevelIterInfos = new ArrayList<>();
334        toplevelValueInfos = new ArrayList<>();
335        canonicalPrefixIterInfos = new HashMap<>();
336
337        uncorrelatedCounter = -1;
338        hasWildcard = false;
339
340        // we do parsing using the ExpressionEvaluator to be sure that references
341        // are visited in the same order as when we'll do actual expression evaluation
342        parsing = true;
343        walkAll();
344        parsing = false;
345
346        // we use all iterators in reversed ordered to increment them lexicographically from the end
347        Collections.reverse(allIterInfos);
348    }
349
350    /**
351     * Returns the projection matches for a given state.
352     */
353    public List<Map<String, Serializable>> matches(State state) {
354        if (!checkSecurity(state)) {
355            return Collections.emptyList();
356        }
357        this.state = state; // needed for mixin types evaluation
358
359        // initializes values and wildcards
360        initializeValuesAndIterators(state);
361
362        List<Map<String, Serializable>> matches = new ArrayList<>();
363        for (;;) {
364            Map<String, Serializable> projection = walkAll();
365            if (projection != null) {
366                matches.add(projection);
367            }
368            if (!hasWildcard) {
369                // all projections will be the same, get at most one
370                break;
371            }
372            boolean finished = incrementIterators();
373            if (finished) {
374                break;
375            }
376        }
377        return matches;
378    }
379
380    protected boolean checkSecurity(State state) {
381        if (principals == null) {
382            return true;
383        }
384        String[] racl = (String[]) state.get(KEY_READ_ACL);
385        if (racl == null) {
386            log.error("NULL racl for " + state.get(KEY_ID));
387            return false;
388        }
389        for (String user : racl) {
390            if (principals.contains(user)) {
391                return true;
392            }
393        }
394        return false;
395    }
396
397    /**
398     * Does one walk of the expression, using the wildcardIndexes currently defined.
399     */
400    protected Map<String, Serializable> walkAll() {
401        refCount = 0;
402        Map<String, Serializable> projection = walkSelectClauseAndOrderBy(selectClause, orderByClause);
403        Object res = walkExpression(expression);
404        if (TRUE.equals(res)) {
405            // returns one match
406            return projection;
407        } else {
408            return null;
409        }
410    }
411
412    /**
413     * Walks the select clause and order by clause, and returns the projection.
414     */
415    public Map<String, Serializable> walkSelectClauseAndOrderBy(SelectClause selectClause,
416            OrderByClause orderByClause) {
417        Map<String, Serializable> projection = new HashMap<>();
418        boolean projectionOnFulltextScore = false;
419        boolean sortOnFulltextScore = false;
420        SelectList elements = selectClause.getSelectList();
421        for (Operand op : elements.values()) {
422            if (op instanceof Reference) {
423                Reference ref = (Reference) op;
424                if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) {
425                    projectionOnFulltextScore = true;
426                }
427                addProjection(ref, projection);
428            }
429        }
430        if (orderByClause != null) {
431            for (OrderByExpr obe : orderByClause.elements) {
432                Reference ref = obe.reference;
433                if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) {
434                    sortOnFulltextScore = true;
435                }
436                addProjection(ref, projection);
437            }
438        }
439        if (projectionOnFulltextScore || sortOnFulltextScore) {
440            if (!parsing) {
441                if (!hasFulltext) {
442                    throw new QueryParseException(
443                            NXQL.ECM_FULLTEXT_SCORE + " cannot be used without " + NXQL.ECM_FULLTEXT);
444                }
445                projection.put(NXQL.ECM_FULLTEXT_SCORE, Double.valueOf(1));
446            }
447        }
448        return projection;
449    }
450
451    protected void addProjection(Reference ref, Map<String, Serializable> projection) {
452        String name = ref.name;
453        if (name.equals(NXQL.ECM_PATH)) {
454            // ecm:path is special, computed and not stored in database
455            if (!parsing) {
456                // to compute PATH we need NAME, ID and PARENT_ID for all states
457                projection.put(NXQL.ECM_NAME, state.get(KEY_NAME));
458                projection.put(NXQL.ECM_UUID, state.get(KEY_ID));
459                projection.put(NXQL.ECM_PARENTID, state.get(KEY_PARENT_ID));
460            }
461            return;
462        }
463        ValueInfo valueInfo = walkReferenceGetValueInfo(ref);
464        if (!parsing) {
465            projection.put(valueInfo.nxqlProp, (Serializable) valueInfo.value);
466        }
467    }
468
469    public boolean hasWildcardProjection() {
470        return selectClause.getSelectList().values().stream().anyMatch(
471                operand -> operand instanceof Reference && ((Reference) operand).name.contains("*"));
472    }
473
474    @Override
475    public Object walkReference(Reference ref) {
476        return walkReferenceGetValueInfo(ref).getValueForEvaluation();
477    }
478
479    protected ValueInfo walkReferenceGetValueInfo(Reference ref) {
480        if (parsing) {
481            ValueInfo valueInfo = parseReference(ref);
482            referenceValueInfos.add(valueInfo);
483            return valueInfo;
484        } else {
485            return referenceValueInfos.get(refCount++);
486        }
487    }
488
489    /**
490     * Parses and computes value and iterator information for a reference.
491     */
492    protected ValueInfo parseReference(Reference ref) {
493        ValueInfo parsed = parseReference(ref.name, ref.originalName);
494        if (DATE_CAST.equals(ref.cast)) {
495            Type type = parsed.type;
496            if (!(type instanceof DateType
497                    || (type instanceof ListType && ((ListType) type).getFieldType() instanceof DateType))) {
498                throw new QueryParseException("Cannot cast to " + ref.cast + ": " + ref.name);
499            }
500            parsed.isDateCast = true;
501        }
502
503        return canonicalReferenceValueInfos.computeIfAbsent(parsed.canonRef, k -> {
504            List<IterInfo> iterInfos = toplevelIterInfos;
505            List<ValueInfo> valueInfos = toplevelValueInfos;
506            List<String> prefix = new ArrayList<>(3); // canonical prefix
507            List<Serializable> steps = new ArrayList<>(1);
508            for (Serializable step : parsed.steps) {
509                if (step instanceof String) {
510                    // complex sub-property
511                    prefix.add((String) step);
512                    steps.add(step);
513                    continue;
514                }
515                if (step instanceof Integer) {
516                    // explicit list index
517                    prefix.add(step.toString());
518                    steps.add(step);
519                    continue;
520                }
521                // wildcard
522                hasWildcard = true;
523                prefix.add("*" + step);
524                String canonPrefix = StringUtils.join(prefix, '/');
525                IterInfo iter = canonicalPrefixIterInfos.get(canonPrefix);
526                if (iter == null) {
527                    // first time we see this wildcard prefix, use a new iterator
528                    iter = new IterInfo(steps);
529                    canonicalPrefixIterInfos.put(canonPrefix, iter);
530                    allIterInfos.add(iter);
531                    iterInfos.add(iter);
532                }
533                iterInfos = iter.dependentIterInfos;
534                valueInfos = iter.dependentValueInfos;
535                // reset traversal for next cycle
536                steps = new ArrayList<>();
537            }
538            // truncate traversal to steps since last wildcard, may be empty if referencing wildcard list directly
539            parsed.steps = steps;
540            valueInfos.add(parsed);
541            return parsed;
542        });
543    }
544
545    /**
546     * Gets the canonical reference and parsed reference for this reference name.
547     * <p>
548     * The parsed reference is a list of components to traverse to get the value:
549     * <ul>
550     * <li>String = map key
551     * <li>Integer = list element
552     * <li>Long = wildcard correlation number (pos/neg)
553     * </ul>
554     *
555     * @return the canonical reference (with resolved uncorrelated wildcards)
556     */
557    protected ValueInfo parseReference(String name, String originalName) {
558
559        if (name.startsWith(NXQL.ECM_TAG)) {
560            if (name.equals(NXQL.ECM_TAG)) {
561                name = FACETED_TAG + "/*1/" + FACETED_TAG_LABEL;
562            } else {
563                name = FACETED_TAG + name.substring(NXQL.ECM_TAG.length()) + "/" + FACETED_TAG_LABEL;
564            }
565        }
566
567        String[] parts = name.split("/");
568
569        // convert first part to internal representation, and canonicalize prefixed schema
570        String prop = parts[0];
571        Type type;
572        boolean isTrueOrNullBoolean;
573        if (prop.startsWith(NXQL.ECM_PREFIX)) {
574            prop = DBSSession.convToInternal(prop);
575            if (prop.equals(KEY_ACP)) {
576                return parseACP(parts, name);
577            }
578            type = DBSSession.getType(prop);
579            isTrueOrNullBoolean = true;
580        } else {
581            Field field = schemaManager.getField(prop);
582            if (field == null) {
583                if (prop.indexOf(':') > -1) {
584                    throw new QueryParseException("No such property: " + name);
585                }
586                // check without prefix
587                // TODO precompute this in SchemaManagerImpl
588                for (Schema schema : schemaManager.getSchemas()) {
589                    if (!StringUtils.isBlank(schema.getNamespace().prefix)) {
590                        // schema with prefix, do not consider as candidate
591                        continue;
592                    }
593                    if (schema != null) {
594                        field = schema.getField(prop);
595                        if (field != null) {
596                            break;
597                        }
598                    }
599                }
600                if (field == null) {
601                    throw new QueryParseException("No such property: " + name);
602                }
603            }
604            type = field.getType();
605            isTrueOrNullBoolean = false;
606            prop = field.getName().getPrefixedName();
607        }
608        parts[0] = prop;
609
610        // canonical prefix used to find shared values (foo/*1 referenced twice always uses the same value)
611        List<String> canonParts = new ArrayList<>(parts.length);
612        List<Serializable> steps = new ArrayList<>(parts.length);
613        boolean firstPart = true;
614        for (String part : parts) {
615            int c = part.indexOf('[');
616            if (c >= 0) {
617                // compat xpath foo[123] -> 123
618                part = part.substring(c + 1, part.length() - 1);
619            }
620            Serializable step;
621            if (NumberUtils.isDigits(part)) {
622                // explicit list index
623                step = Integer.valueOf(part);
624                type = ((ListType) type).getFieldType();
625            } else if (!part.startsWith("*")) {
626                // complex sub-property
627                step = part;
628                if (firstPart) {
629                    if (PROP_MAJOR_VERSION.equals(part) || PROP_MINOR_VERSION.equals(part)) {
630                        step = DBSSession.convToInternal(part);
631                    }
632                    // we already computed the type of the first part
633                } else {
634                    Field field = ((ComplexType) type).getField(part);
635                    if (field == null) {
636                        throw new QueryParseException("No such property: " + name);
637                    }
638                    type = field.getType();
639                }
640            } else {
641                // wildcard
642                int corr;
643                if (part.length() == 1) {
644                    // uncorrelated wildcard
645                    corr = uncorrelatedCounter--; // negative
646                    part = "*" + corr; // unique correlation
647                } else {
648                    // correlated wildcard, use correlation number
649                    String digits = part.substring(1);
650                    if (!NumberUtils.isDigits(digits)) {
651                        throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name);
652                    }
653                    corr = Integer.parseInt(digits);
654                    if (corr < 0) {
655                        throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name);
656                    }
657                }
658                step = Long.valueOf(corr);
659                type = ((ListType) type).getFieldType();
660            }
661            canonParts.add(part);
662            steps.add(step);
663            firstPart = false;
664        }
665        String canonRef = StringUtils.join(canonParts, '/');
666        String nxqlProp = originalName == null ? name : originalName;
667        ValueInfo valueInfo = new ValueInfo(steps, nxqlProp, canonRef);
668        valueInfo.type = type;
669        valueInfo.isTrueOrNullBoolean = isTrueOrNullBoolean;
670        return valueInfo;
671    }
672
673    protected ValueInfo parseACP(String[] parts, String name) {
674        if (parts.length != 3) {
675            throw new QueryParseException("No such property: " + name);
676        }
677
678        String wildcard = parts[1];
679        if (NumberUtils.isDigits(wildcard)) {
680            throw new QueryParseException("Cannot use explicit index in ACLs: " + name);
681        }
682        int corr;
683        if (wildcard.length() == 1) {
684            // uncorrelated wildcard
685            corr = uncorrelatedCounter--; // negative
686            wildcard = "*" + corr; // unique correlation
687        } else {
688            // correlated wildcard, use correlation number
689            String digits = wildcard.substring(1);
690            if (!NumberUtils.isDigits(digits)) {
691                throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name);
692            }
693            corr = Integer.parseInt(digits);
694            if (corr < 0) {
695                throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name);
696            }
697        }
698
699        String subPart = DBSSession.convToInternalAce(parts[2]);
700        if (subPart == null) {
701            throw new QueryParseException("No such property: " + name);
702        }
703        List<Serializable> steps;
704        String canonRef;
705        if (subPart.equals(KEY_ACL_NAME)) {
706            steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL_NAME));
707            canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL_NAME;
708        } else {
709            // for the second iterator we want a correlation number tied to the first one
710            int corr2 = corr * 1000000;
711            String wildcard2 = "*" + corr2;
712            steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL, Long.valueOf(corr2), subPart));
713            canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL + '/' + wildcard2 + '/' + subPart;
714        }
715        ValueInfo valueInfo = new ValueInfo(steps, name, canonRef);
716        valueInfo.type = DBSSession.getType(subPart);
717        valueInfo.isTrueOrNullBoolean = false; // TODO check ok
718        return valueInfo;
719    }
720
721    /**
722     * Initializes toplevel values and iterators for a given state.
723     */
724    protected void initializeValuesAndIterators(State state) {
725        init(state, toplevelValueInfos, toplevelIterInfos);
726    }
727
728    /**
729     * Initializes values and iterators for a given state.
730     */
731    protected void init(Object state, List<ValueInfo> valueInfos, List<IterInfo> iterInfos) {
732        for (ValueInfo valueInfo : valueInfos) {
733            valueInfo.value = traverse(state, valueInfo.steps);
734        }
735        for (IterInfo iterInfo : iterInfos) {
736            Object value = traverse(state, iterInfo.steps);
737            iterInfo.setList(value);
738            Object iterState = iterInfo.hasNext() ? iterInfo.next() : null;
739            init(iterState, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos);
740        }
741    }
742
743    /**
744     * Traverses an object in a series of steps.
745     */
746    protected Object traverse(Object value, List<Serializable> steps) {
747        for (Serializable step : steps) {
748            value = traverse(value, step);
749        }
750        return value;
751    }
752
753    /**
754     * Traverses a single step.
755     */
756    protected Object traverse(Object value, Serializable step) {
757        if (step instanceof String) {
758            // complex sub-property
759            if (value != null && !(value instanceof State)) {
760                throw new QueryParseException("Invalid property " + step + " (no State but " + value.getClass() + ")");
761            }
762            return value == null ? null : ((State) value).get(step);
763        } else if (step instanceof Integer) {
764            // explicit list index
765            int index = ((Integer) step).intValue();
766            if (value == null) {
767                return null;
768            } else if (value instanceof List) {
769                @SuppressWarnings("unchecked")
770                List<Serializable> list = (List<Serializable>) value;
771                if (index >= list.size()) {
772                    return null;
773                } else {
774                    return list.get(index);
775                }
776            } else if (value instanceof Object[]) {
777                Object[] array = (Object[]) value;
778                if (index >= array.length) {
779                    return null;
780                } else {
781                    return array[index];
782                }
783            } else {
784                throw new QueryParseException(
785                        "Invalid property " + step + " (no List/array but " + value.getClass() + ")");
786            }
787        } else {
788            throw new QueryParseException("Invalid step " + step + " (unknown class " + step.getClass() + ")");
789        }
790    }
791
792    /**
793     * Increments iterators lexicographically.
794     * <p>
795     * Returns {@code true} when all iterations are finished.
796     */
797    protected boolean incrementIterators() {
798        // we iterate on a pre-reversed allIterInfos list as this ensure that
799        // dependent iterators are incremented before those that control them
800        boolean more = false;
801        for (IterInfo iterInfo : allIterInfos) {
802            more = iterInfo.hasNext();
803            if (!more) {
804                // end of this iterator, reset and !more will carry to next one
805                iterInfo.reset();
806            }
807            // get the current value, if any
808            Object state = iterInfo.hasNext() ? iterInfo.next() : null;
809            // recompute dependent stuff
810            init(state, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos);
811            if (more) {
812                break;
813            }
814        }
815        return !more;
816    }
817
818    /**
819     * {@inheritDoc}
820     * <p>
821     * ecm:mixinTypes IN ('Foo', 'Bar')
822     * <p>
823     * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%'
824     * <p>
825     * ecm:mixinTypes NOT IN ('Foo', 'Bar')
826     * <p>
827     * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE
828     * '%Bar%' OR mixintypes IS NULL)
829     */
830    @Override
831    public Boolean walkMixinTypes(List<String> mixins, boolean include) {
832        if (parsing) {
833            return null;
834        }
835        /*
836         * Primary types that match.
837         */
838        Set<String> matchPrimaryTypes;
839        if (include) {
840            matchPrimaryTypes = new HashSet<>();
841            for (String mixin : mixins) {
842                matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin));
843            }
844        } else {
845            matchPrimaryTypes = new HashSet<>(getDocumentTypes());
846            for (String mixin : mixins) {
847                matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin));
848            }
849        }
850        /*
851         * Instance mixins that match.
852         */
853        Set<String> matchMixinTypes = new HashSet<>();
854        for (String mixin : mixins) {
855            if (!isNeverPerInstanceMixin(mixin)) {
856                matchMixinTypes.add(mixin);
857            }
858        }
859        /*
860         * Evaluation.
861         */
862        String primaryType = (String) state.get(KEY_PRIMARY_TYPE);
863        Object[] mixinTypesArray = (Object[]) state.get(KEY_MIXIN_TYPES);
864        List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray);
865        if (include) {
866            // primary types
867            if (matchPrimaryTypes.contains(primaryType)) {
868                return TRUE;
869            }
870            // mixin types
871            matchMixinTypes.retainAll(mixinTypes); // intersection
872            return Boolean.valueOf(!matchMixinTypes.isEmpty());
873        } else {
874            // primary types
875            if (!matchPrimaryTypes.contains(primaryType)) {
876                return FALSE;
877            }
878            // mixin types
879            matchMixinTypes.retainAll(mixinTypes); // intersection
880            return Boolean.valueOf(matchMixinTypes.isEmpty());
881        }
882    }
883
884    @Override
885    public String toString() {
886        StringBuilder sb = new StringBuilder("SELECT ");
887        sb.append(selectClause);
888        sb.append(" WHERE ");
889        if (expression instanceof MultiExpression) {
890            for (Iterator<Operand> it = ((MultiExpression) expression).values.iterator(); it.hasNext();) {
891                Operand operand = it.next();
892                sb.append(operand.toString());
893                if (it.hasNext()) {
894                    sb.append(" AND ");
895                }
896            }
897        } else {
898            sb.append(expression);
899        }
900        if (orderByClause != null) {
901            sb.append(" ORDER BY ");
902            sb.append(orderByClause);
903        }
904        return sb.toString();
905    }
906
907}