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