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}