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