001/* 002 * (C) Copyright 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.directory.multi; 020 021import java.util.Arrays; 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.nuxeo.ecm.core.api.DocumentModelList; 030import org.nuxeo.ecm.core.query.QueryParseException; 031import org.nuxeo.ecm.core.query.sql.model.Expression; 032import org.nuxeo.ecm.core.query.sql.model.IdentityQueryTransformer; 033import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 034import org.nuxeo.ecm.core.query.sql.model.Operand; 035import org.nuxeo.ecm.core.query.sql.model.Operator; 036import org.nuxeo.ecm.core.query.sql.model.Predicate; 037import org.nuxeo.ecm.core.query.sql.model.QueryBuilder; 038import org.nuxeo.ecm.core.query.sql.model.Reference; 039import org.nuxeo.ecm.directory.Session; 040import org.nuxeo.ecm.directory.multi.MultiDirectorySession.SourceInfo; 041import org.nuxeo.ecm.directory.multi.MultiDirectorySession.SubDirectoryInfo; 042 043/** 044 * Evaluator for an {@link Expression} in the context of the various subdirectories of a MultiDirectory's source. 045 * <p> 046 * The result is a set of entry ids. 047 * <p> 048 * The strategy for evaluation is to delegate as much as possible of the evaluation of expressions to subdirectories 049 * themselves. 050 * <p> 051 * We do a depth-first evaluation of expressions, delaying actual evaluation while an expression's references all fall 052 * into the same subdirectory. 053 * 054 * @since 10.3 055 */ 056public class MultiDirectoryExpressionEvaluator { 057 058 /** The result of an evaluation of an expression. */ 059 public interface Result { 060 } 061 062 /** Result is a set of entry ids. */ 063 public static class IdsResult implements Result { 064 public final Set<String> ids; 065 066 public IdsResult(Set<String> ids) { 067 this.ids = ids; 068 } 069 } 070 071 /** Result is an operand associated to at most one subdirectory. */ 072 public static class OperandResult implements Result { 073 074 public final Operand operand; 075 076 public final boolean hasId; 077 078 public final String dir; // may be null 079 080 public OperandResult(Operand operand, boolean hasId, String dir) { 081 this.operand = operand; 082 this.hasId = hasId; 083 this.dir = dir; 084 } 085 } 086 087 protected final List<SubDirectoryInfo> dirInfos; 088 089 protected final String idField; 090 091 protected final String dirName; // for error messages 092 093 public MultiDirectoryExpressionEvaluator(SourceInfo sourceInfo, String idField, String dirName) { 094 dirInfos = sourceInfo.subDirectoryInfos; 095 this.idField = idField; 096 this.dirName = dirName; 097 098 } 099 100 /** 101 * Evaluates an expression and returns the set of matching ids. 102 */ 103 public Set<String> eval(Expression expr) { 104 return evaluate(evalExpression(expr)); 105 } 106 107 protected Result evalExpression(Expression expr) { 108 Operator op = expr.operator; 109 if (expr instanceof MultiExpression) { 110 return evalMultiExpression((MultiExpression) expr); 111 } else if (op == Operator.AND || op == Operator.OR) { 112 return evalAndOr(expr); 113 } else { 114 return evalSimpleExpression(expr); 115 } 116 } 117 118 protected Result evalSimpleExpression(Expression expr) { 119 Result left = evalOperand(expr.lvalue); 120 Result right = evalOperand(expr.rvalue); 121 122 // case where we can return a single-dir operand to the caller 123 if (left instanceof OperandResult && right instanceof OperandResult) { 124 // check id and subdirectories 125 OperandResult lop = (OperandResult) left; 126 OperandResult rop = (OperandResult) right; 127 if (lop.dir == null || rop.dir == null || lop.dir.equals(rop.dir)) { 128 // still one subdirectory 129 String dir = lop.dir == null ? rop.dir : lop.dir; 130 return new OperandResult(expr, lop.hasId || rop.hasId, dir); 131 } 132 } 133 134 // else for a simple expression we have no way of doing manual evaluation 135 throw new QueryParseException("Invalid expression for multidirectory: " + expr); 136 } 137 138 protected Result evalOperand(Operand op) { 139 if (op instanceof Expression) { 140 return evalExpression((Expression) op); 141 } else if (op instanceof Reference) { 142 return evalReference((Reference) op); 143 } else { // Literal / LiteralList / Function 144 return new OperandResult(op, false, null); 145 } 146 } 147 148 protected Result evalReference(Reference ref) { 149 String name = ref.name; 150 if (name.equals(idField)) { 151 return new OperandResult(ref, true, null); 152 } 153 for (SubDirectoryInfo dirInfo : dirInfos) { 154 if (dirInfo.fromSource.containsKey(name)) { 155 return new OperandResult(ref, false, dirInfo.dirName); 156 } 157 } 158 throw new QueryParseException("No column: " + name + " for directory: " + dirName); 159 } 160 161 protected Result evalAndOr(Expression expr) { 162 List<Predicate> predicates = Arrays.asList((Predicate) expr.lvalue, (Predicate) expr.rvalue); 163 return evalMultiExpression(new MultiExpression(expr.operator, predicates)); 164 } 165 166 protected Result evalMultiExpression(MultiExpression expr) { 167 boolean and = expr.operator == Operator.AND; 168 List<Predicate> predicates = expr.predicates; 169 Iterator<Predicate> it = predicates.iterator(); 170 if (!it.hasNext()) { 171 // empty multiexpression 172 return new OperandResult(expr, false, null); 173 } 174 Result previous = evalExpression(it.next()); 175 while (it.hasNext()) { 176 if (and && previous instanceof IdsResult && ((IdsResult) previous).ids.isEmpty()) { 177 // optimization, no need to do more work 178 return previous; 179 } 180 Result next = evalExpression(it.next()); 181 182 // check if we can keep a single-dir operand 183 if (previous instanceof OperandResult && next instanceof OperandResult) { 184 // check id and subdirectories 185 OperandResult prv = (OperandResult) previous; 186 OperandResult nxt = (OperandResult) next; 187 if (prv.dir == null || nxt.dir == null || prv.dir.equals(nxt.dir)) { 188 // still one subdirectory 189 String dir = prv.dir == null ? nxt.dir : prv.dir; 190 previous = new OperandResult(expr, prv.hasId || nxt.hasId, dir); 191 continue; 192 } 193 } 194 195 // turn everything into ids and do intersection/union 196 Set<String> previousIds = evaluate(previous); 197 if (and && previousIds.isEmpty()) { 198 // optimization, no need to do more work 199 return new IdsResult(previousIds); 200 } 201 Set<String> nextIds = evaluate(next); 202 Set<String> ids = and ? intersection(previousIds, nextIds) : union(previousIds, nextIds); 203 previous = new IdsResult(ids); 204 } 205 return previous; 206 } 207 208 /** 209 * Evaluates a result and returns the set of matching ids. 210 */ 211 protected Set<String> evaluate(Result result) { 212 if (result instanceof IdsResult) { 213 return ((IdsResult) result).ids; 214 } else { 215 return evaluate((OperandResult) result); 216 } 217 } 218 219 /** 220 * Evaluates an operand associated to a single directory and returns the set of matching ids. 221 */ 222 protected Set<String> evaluate(OperandResult opr) { 223 // find subdirectory to use 224 SubDirectoryInfo subDirInfo = null; 225 for (SubDirectoryInfo dirInfo : dirInfos) { 226 if (opr.dir != null) { 227 if (opr.dir.equals(dirInfo.dirName)) { 228 subDirInfo = dirInfo; 229 break; 230 } 231 } else { 232 // expression without any reference (except maybe id), pick any non-optional directory 233 if (!dirInfo.isOptional) { 234 subDirInfo = dirInfo; 235 break; 236 } 237 } 238 } 239 if (subDirInfo == null) { 240 throw new QueryParseException( 241 "Configuration error: no non-optional subdirectory for multidirectory: " + dirName); 242 } 243 // map field names from multidirectory to subdirectory 244 Predicate predicate = new ReferenceRenamer(subDirInfo.fromSource).transform((Predicate) opr.operand); 245 QueryBuilder queryBuilder = new QueryBuilder(); 246 if (predicate instanceof MultiExpression) { 247 queryBuilder.filter((MultiExpression) predicate); 248 } else { 249 queryBuilder.predicate(predicate); 250 } 251 try (Session s = subDirInfo.getSession()) { 252 return new HashSet<>(s.queryIds(queryBuilder)); 253 } 254 } 255 256 /** 257 * Renames the references according to a map. 258 * 259 * @since 10.3 260 */ 261 public static class ReferenceRenamer extends IdentityQueryTransformer { 262 263 protected final Map<String, String> map; 264 265 public ReferenceRenamer(Map<String, String> map) { 266 this.map = map; 267 } 268 269 @Override 270 public Reference transform(Reference node) { 271 String name = node.name; 272 String newName = map.getOrDefault(name, name); 273 if (newName.equals(name)) { 274 return node; 275 } else { 276 return new Reference(newName, node.cast, node.esHint); 277 } 278 } 279 } 280 281 /** 282 * Set union. 283 */ 284 protected static Set<String> union(Set<String> a, Set<String> b) { 285 if (a.isEmpty()) { 286 return Collections.emptySet(); 287 } else if (b.isEmpty()) { 288 return Collections.emptySet(); 289 } else { 290 Set<String> set = new HashSet<>(a); 291 if (set.addAll(b)) { 292 return set; 293 } else { 294 return a; // optimization, don't return a new set if there was no change 295 } 296 } 297 } 298 299 /** 300 * Set intersection. 301 */ 302 protected static Set<String> intersection(Set<String> a, Set<String> b) { 303 if (a.isEmpty()) { 304 return Collections.emptySet(); 305 } else if (b.isEmpty()) { 306 return Collections.emptySet(); 307 } else { 308 Set<String> set = new HashSet<>(a); 309 if (set.retainAll(b)) { 310 return set; 311 } else { 312 return a; // optimization, don't return a new set if there was no change 313 } 314 } 315 } 316 317}