001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.core.storage.dbs;
018
019import static java.lang.Boolean.FALSE;
020import static java.lang.Boolean.TRUE;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.query.QueryParseException;
035import org.nuxeo.ecm.core.query.sql.NXQL;
036import org.nuxeo.ecm.core.query.sql.model.Expression;
037import org.nuxeo.ecm.core.query.sql.model.OrderByClause;
038import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
039import org.nuxeo.ecm.core.query.sql.model.OrderByList;
040import org.nuxeo.ecm.core.query.sql.model.Reference;
041import org.nuxeo.ecm.core.query.sql.model.SelectClause;
042import org.nuxeo.ecm.core.schema.DocumentType;
043import org.nuxeo.ecm.core.schema.SchemaManager;
044import org.nuxeo.ecm.core.schema.types.Field;
045import org.nuxeo.ecm.core.schema.types.ListType;
046import org.nuxeo.ecm.core.schema.types.Schema;
047import org.nuxeo.ecm.core.schema.types.Type;
048import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
049import org.nuxeo.ecm.core.storage.ExpressionEvaluator;
050import org.nuxeo.ecm.core.storage.State;
051import org.nuxeo.runtime.api.Framework;
052
053/**
054 * Expression evaluator for a {@link DBSDocument} state.
055 *
056 * @since 5.9.4
057 */
058public class DBSExpressionEvaluator extends ExpressionEvaluator {
059
060    private static final Log log = LogFactory.getLog(DBSExpressionEvaluator.class);
061
062    private static final Long ZERO = Long.valueOf(0);
063
064    private static final Long ONE = Long.valueOf(1);
065
066    protected final SelectClause selectClause;
067
068    protected final Expression expression;
069
070    protected final SchemaManager schemaManager;
071
072    protected List<String> documentTypes;
073
074    protected State state;
075
076    public DBSExpressionEvaluator(DBSSession session, SelectClause selectClause, Expression expression, String[] principals) {
077        super(new DBSPathResolver(session), principals);
078        this.selectClause = selectClause;
079        this.expression = expression;
080        schemaManager = Framework.getLocalService(SchemaManager.class);
081    }
082
083    protected static class DBSPathResolver implements PathResolver {
084        protected final DBSSession session;
085
086        public DBSPathResolver(DBSSession session) {
087            this.session = session;
088        }
089
090        @Override
091        public String getIdForPath(String path) {
092            return session.getDocumentIdByPath(path);
093        }
094    }
095
096    protected List<String> getDocumentTypes() {
097        // TODO precompute in SchemaManager
098        if (documentTypes == null) {
099            documentTypes = new ArrayList<>();
100            for (DocumentType docType : schemaManager.getDocumentTypes()) {
101                documentTypes.add(docType.getName());
102            }
103        }
104        return documentTypes;
105    }
106
107    protected Set<String> getMixinDocumentTypes(String mixin) {
108        return schemaManager.getDocumentTypeNamesForFacet(mixin);
109    }
110
111    protected boolean isNeverPerInstanceMixin(String mixin) {
112        return schemaManager.getNoPerDocumentQueryFacets().contains(mixin);
113    }
114
115    public boolean matches(State state) {
116        this.state = state;
117        // security check
118        if (principals != null) {
119            String[] racl = (String[]) walkReference(new Reference(NXQL_ECM_READ_ACL));
120            if (racl == null) {
121                log.error("NULL racl for " + state.get(DBSDocument.KEY_ID));
122            } else {
123                boolean allowed = false;
124                for (String user : racl) {
125                    if (principals.contains(user)) {
126                        allowed = true;
127                        break;
128                    }
129                }
130                if (!allowed) {
131                    return false;
132                }
133            }
134        }
135        return TRUE.equals(walkExpression(expression));
136    }
137
138    public boolean matches(DBSDocumentState docState) {
139        return matches(docState.getState());
140    }
141
142    @Override
143    public Object walkReference(Reference ref) {
144        return evaluateReference(ref, state);
145    }
146
147    /**
148     * Evaluates a reference over the given state.
149     *
150     * @param ref the reference
151     * @param map the state representation
152     */
153    protected Object evaluateReference(Reference ref, State state) {
154        String name = ref.name;
155        String[] split = name.split("/");
156        String prop = split[0];
157        boolean isArray;
158        boolean isBoolean;
159        boolean isTrueOrNullBoolean;
160        if (name.startsWith(NXQL.ECM_PREFIX)) {
161            prop = DBSSession.convToInternal(name);
162            isArray = DBSSession.isArray(prop);
163            isBoolean = DBSSession.isBoolean(prop);
164            isTrueOrNullBoolean = true;
165        } else {
166            Field field = schemaManager.getField(prop);
167            if (field == null) {
168                if (prop.indexOf(':') > -1) {
169                    throw new QueryParseException("No such property: " + name);
170                }
171                // check without prefix
172                // TODO precompute this in SchemaManagerImpl
173                for (Schema schema : schemaManager.getSchemas()) {
174                    if (!StringUtils.isBlank(schema.getNamespace().prefix)) {
175                        // schema with prefix, do not consider as candidate
176                        continue;
177                    }
178                    if (schema != null) {
179                        field = schema.getField(prop);
180                        if (field != null) {
181                            break;
182                        }
183                    }
184                }
185                if (field == null) {
186                    throw new QueryParseException("No such property: " + name);
187                }
188            }
189            prop = field.getName().getPrefixedName();
190            Type type = field.getType();
191            isArray = type instanceof ListType && ((ListType) type).isArray();
192            isBoolean = type instanceof BooleanType;
193            isTrueOrNullBoolean = false;
194            if (isArray && split[split.length - 1].startsWith("*")) {
195                split = Arrays.copyOfRange(split, 0, split.length - 1);
196            }
197        }
198        Serializable value = state.get(prop);
199        for (int i = 1; i < split.length; i++) {
200            if (value == null) {
201                return null;
202            }
203            if (!(value instanceof State)) {
204                throw new QueryParseException("No such property (no State): " + name);
205            }
206            value = ((State) value).get(split[i]);
207        }
208        if (value == null && isArray) {
209            // don't use null, as list-based matches don't use ternary logic
210            value = new Object[0];
211        }
212        if (isBoolean) {
213            // boolean evaluation is like 0 / 1
214            if (isTrueOrNullBoolean) {
215                value = TRUE.equals(value) ? ONE : ZERO;
216            } else {
217                value = value == null ? null : (((Boolean) value).booleanValue() ? ONE : ZERO);
218            }
219        }
220        return value;
221    }
222
223    /**
224     * {@inheritDoc}
225     * <p>
226     * ecm:mixinTypes IN ('Foo', 'Bar')
227     * <p>
228     * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%'
229     * <p>
230     * ecm:mixinTypes NOT IN ('Foo', 'Bar')
231     * <p>
232     * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE
233     * '%Bar%' OR mixintypes IS NULL)
234     */
235    @Override
236    public Boolean walkMixinTypes(List<String> mixins, boolean include) {
237        /*
238         * Primary types that match.
239         */
240        Set<String> matchPrimaryTypes;
241        if (include) {
242            matchPrimaryTypes = new HashSet<String>();
243            for (String mixin : mixins) {
244                matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin));
245            }
246        } else {
247            matchPrimaryTypes = new HashSet<String>(getDocumentTypes());
248            for (String mixin : mixins) {
249                matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin));
250            }
251        }
252        /*
253         * Instance mixins that match.
254         */
255        Set<String> matchMixinTypes = new HashSet<String>();
256        for (String mixin : mixins) {
257            if (!isNeverPerInstanceMixin(mixin)) {
258                matchMixinTypes.add(mixin);
259            }
260        }
261        /*
262         * Evaluation.
263         */
264        String primaryType = (String) state.get(DBSDocument.KEY_PRIMARY_TYPE);
265        Object[] mixinTypesArray = (Object[]) state.get(DBSDocument.KEY_MIXIN_TYPES);
266        List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray);
267        if (include) {
268            // primary types
269            if (matchPrimaryTypes.contains(primaryType)) {
270                return TRUE;
271            }
272            // mixin types
273            matchMixinTypes.retainAll(mixinTypes); // intersection
274            return Boolean.valueOf(!matchMixinTypes.isEmpty());
275        } else {
276            // primary types
277            if (!matchPrimaryTypes.contains(primaryType)) {
278                return FALSE;
279            }
280            // mixin types
281            matchMixinTypes.retainAll(mixinTypes); // intersection
282            return Boolean.valueOf(matchMixinTypes.isEmpty());
283        }
284    }
285
286    public static class OrderByComparator implements Comparator<State> {
287
288        protected final OrderByClause orderByClause;
289
290        protected DBSExpressionEvaluator evaluator;
291
292        public OrderByComparator(OrderByClause orderByClause, DBSExpressionEvaluator evaluator) {
293            // replace ecm:path with ecm:__path for evaluation
294            // (we don't want to allow ecm:path to be usable anywhere else
295            // and resolve to a null value)
296            OrderByList obl = new OrderByList(null); // stupid constructor
297            obl.clear();
298            for (OrderByExpr ob : orderByClause.elements) {
299                if (ob.reference.name.equals(NXQL.ECM_PATH)) {
300                    ob = new OrderByExpr(new Reference(NXQL_ECM_PATH), ob.isDescending);
301                }
302                obl.add(ob);
303            }
304            this.orderByClause = new OrderByClause(obl);
305            this.evaluator = evaluator;
306        }
307
308        @Override
309        public int compare(State s1, State s2) {
310            for (OrderByExpr ob : orderByClause.elements) {
311                Reference ref = ob.reference;
312                boolean desc = ob.isDescending;
313                int sign = desc ? -1 : 1;
314                Object v1 = evaluator.evaluateReference(ref, s1);
315                Object v2 = evaluator.evaluateReference(ref, s2);
316                if (v1 == null) {
317                    return v2 == null ? 0 : -sign;
318                } else if (v2 == null) {
319                    return sign;
320                } else {
321                    if (!(v1 instanceof Comparable)) {
322                        throw new QueryParseException("Not a comparable: " + v1);
323                    }
324                    int cmp = ((Comparable<Object>) v1).compareTo(v2);
325                    return desc ? -cmp : cmp;
326                }
327            }
328            return 0;
329        }
330    }
331
332}