/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr.index.local; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.NavigableMap; import java.util.NoSuchElementException; import java.util.Set; import javax.jcr.query.qom.And; import javax.jcr.query.qom.Constraint; import javax.jcr.query.qom.Not; import javax.jcr.query.qom.Or; import javax.jcr.query.qom.PropertyExistence; import javax.jcr.query.qom.StaticOperand; import org.modeshape.common.collection.EmptyIterator; import org.modeshape.common.collection.MultiIterator; import org.modeshape.common.logging.Logger; import org.modeshape.jcr.api.query.qom.Between; import org.modeshape.jcr.api.query.qom.Operator; import org.modeshape.jcr.api.query.qom.SetCriteria; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.index.local.IndexValues.Converter; import org.modeshape.jcr.query.model.BindVariableName; import org.modeshape.jcr.query.model.Comparison; import org.modeshape.jcr.query.model.Literal; import org.modeshape.jcr.spi.index.Index; import org.modeshape.jcr.spi.index.provider.Filter; import org.modeshape.jcr.spi.index.provider.Filter.Results; /** * Utility for building {@link Results index Operation} instances that will use an index to return those {@link NodeKey}s that * satisfy a set of criteria. * * @author Randall Hauch (rhauch@redhat.com) */ class Operations { protected static final Logger LOGGER = Logger.getLogger(Operations.class); protected static final Results EMPTY_RESULTS = new Results() { @Override public Filter.ResultBatch getNextBatch(int batchSize) { return Filter.ResultBatch.EMPTY; } @Override public void close() { } }; protected static final FilterOperation EMPTY_FILTER_OPERATION = new FilterOperation() { @Override public Results getResults() { return EMPTY_RESULTS; } @Override public long estimateCount() { return 0; } }; /** * Create an {@link Results index operation} instance that will use the supplied {@link NavigableMap} (provided by an index) * and the {@link Converter} to return all of the {@link NodeKey}s that satisfy the given constraints. * * @param keysByValue the index's map of values-to-NodeKey; may not be null * @param converter the converter; may not be null * @param constraints the constraints; may not be null but may be empty if there are no constraints * @param variables the bound variables for this query; may not be null but may be empty * @return the index operation; never null */ public static <T> FilterOperation createFilter( NavigableMap<T, String> keysByValue, Converter<T> converter, Collection<Constraint> constraints, Map<String, Object> variables ) { if (keysByValue.isEmpty()) return EMPTY_FILTER_OPERATION; NodeKeysAccessor<T, String> nodeKeysAccessor = new NodeKeysAccessor<T, String>() { @Override public Iterator<String> getNodeKeys( NavigableMap<T, String> keysByValue ) { return keysByValue.values().iterator(); } @Override public void addAllTo( NavigableMap<T, String> keysByValue, Set<String> matchedKeys ) { matchedKeys.addAll(keysByValue.values()); } }; OperationBuilder<T> builder = new BasicOperationBuilder<>(keysByValue, converter, nodeKeysAccessor, variables); for (Constraint constraint : constraints) { OperationBuilder<T> newBuilder = builder.apply(constraint, false); if (newBuilder != null) builder = newBuilder; } return builder; } /** * Create an {@link Results index operation} instance that will use the supplied {@link NavigableMap} (provided by an * enumerated index) and the {@link Converter} to return all of the {@link NodeKey}s that satisfy the given constraints. * * @param keySetByEnumeratedValue the index's map of NodeKey sets; may not be null * @param converter the converter; may not be null * @param constraints the constraints; may not be null but may be empty if there are no constraints * @param variables the bound variables for this query; may not be null but may be empty * @return the index operation; never null */ public static <T> FilterOperation createEnumeratedFilter( NavigableMap<T, Set<String>> keySetByEnumeratedValue, Converter<T> converter, Collection<Constraint> constraints, Map<String, Object> variables ) { if (keySetByEnumeratedValue.isEmpty()) return EMPTY_FILTER_OPERATION; NodeKeysAccessor<T, Set<String>> nodeKeysAccessor = new NodeKeysAccessor<T, Set<String>>() { @Override public Iterator<String> getNodeKeys( NavigableMap<T, Set<String>> keysByValue ) { if (keysByValue.isEmpty()) return new EmptyIterator<String>(); if (keysByValue.size() == 1) return keysByValue.values().iterator().next().iterator(); return MultiIterator.fromIterables(keysByValue.values()); } @Override public void addAllTo( NavigableMap<T, Set<String>> keysByValue, Set<String> matchedKeys ) { for (Map.Entry<T, Set<String>> entry : keysByValue.entrySet()) { matchedKeys.addAll(entry.getValue()); } } }; OperationBuilder<T> builder = new BasicOperationBuilder<>(keySetByEnumeratedValue, converter, nodeKeysAccessor, variables); for (Constraint constraint : constraints) { OperationBuilder<T> newBuilder = builder.apply(constraint, false); if (newBuilder != null) builder = newBuilder; } return builder; } public static interface FilterOperation { Index.Results getResults(); /** * Obtain an estimate of the number of results. * * @return the estimated number of results; either 0 or a positive number */ long estimateCount(); } /** * A builder of {@link Results} instances. Builders are used to apply multiple {@link Constraint}s to an index and to * ultimiately {@link #getResults() get} a {@link Results} instance that can be used to obtain the values that satisfy the * constraints. * * @param <T> the type of index key * @author Randall Hauch (rhauch@redhat.com) */ protected abstract static class OperationBuilder<T> implements FilterOperation { protected OperationBuilder() { } /** * Return a new operation builder that will apply the given constraint to the index, possibly negating the constraint. * * @param constraint the constraint; may not be null * @param negated true if the constraint should be negated, or false if it should be applied as-is * @return the builder; never null */ public OperationBuilder<T> apply( Constraint constraint, boolean negated ) { if (constraint instanceof Between) return apply((Between)constraint, negated); if (constraint instanceof Comparison) return apply((Comparison)constraint, negated); if (constraint instanceof And) { And and = (And)constraint; return apply(and.getConstraint1(), negated).apply(and.getConstraint2(), negated); } if (constraint instanceof Or) { Or or = (Or)constraint; OperationBuilder<T> left = apply(or.getConstraint1(), negated); OperationBuilder<T> right = apply(or.getConstraint2(), negated); return new DualOperationBuilder<>(left, right); } if (constraint instanceof Not) { Not not = (Not)constraint; return apply(not.getConstraint(), !negated); } if (constraint instanceof PropertyExistence) { // Presumably this index only contains values for this property ... return this; } if (constraint instanceof SetCriteria) { SetCriteria criteria = (SetCriteria)constraint; return apply(criteria, negated); } // We don't know how to handle any of the other kinds of constraints ... LOGGER.debug("Unable to process constraint, so ignoring: {0}", constraint); return this; } protected abstract OperationBuilder<T> apply( Between between, boolean negated ); protected abstract OperationBuilder<T> apply( Comparison comparison, boolean negated ); protected abstract OperationBuilder<T> apply( SetCriteria setCriteria, boolean negated ); } protected static interface NodeKeysAccessor<T, V> { public Iterator<String> getNodeKeys( NavigableMap<T, V> keysByValue ); public void addAllTo( NavigableMap<T, V> keysByValue, Set<String> matchedKeys ); } /** * This builder operates against the supplied {@link NavigableMap} and for each constraint will find the * {@link NavigableMap#subMap(Object, boolean, Object, boolean) submap} that satisfies the constraints. * * @param <T> the type of index key * @param <V> the type of value to be iterated over * @author Randall Hauch (rhauch@redhat.com) */ protected static class BasicOperationBuilder<T, V> extends OperationBuilder<T> { protected final NavigableMap<T, V> keysByValue; protected final Converter<T> converter; protected final NodeKeysAccessor<T, V> nodeKeysAccessor; protected final Map<String, Object> variables; protected BasicOperationBuilder( NavigableMap<T, V> keysByValue, Converter<T> converter, NodeKeysAccessor<T, V> nodeKeysAccessor, Map<String, Object> variables ) { this.keysByValue = keysByValue; this.converter = converter; this.nodeKeysAccessor = nodeKeysAccessor; this.variables = variables; } protected OperationBuilder<T> create( NavigableMap<T, V> keysByValue ) { return new BasicOperationBuilder<>(keysByValue, converter, nodeKeysAccessor, variables); } @Override protected OperationBuilder<T> apply( Between between, boolean negated ) { T lower = converter.toLowerValue(between.getLowerBound(), variables); T upper = converter.toUpperValue(between.getUpperBound(), variables); boolean isLowerIncluded = between.isLowerBoundIncluded(); boolean isUpperIncluded = between.isUpperBoundIncluded(); if (negated) { OperationBuilder<T> lowerOp = create(keysByValue.headMap(lower, !isLowerIncluded)); OperationBuilder<T> upperOp = create(keysByValue.tailMap(upper, !isUpperIncluded)); return new DualOperationBuilder<>(lowerOp, upperOp); } return create(keysByValue.subMap(lower, isLowerIncluded, upper, isUpperIncluded)); } @Override protected OperationBuilder<T> apply( Comparison comparison, boolean negated ) { StaticOperand operand = comparison.getOperand2(); Operator op = comparison.operator(); if (negated) op = op.not(); // Remember that T may be a composite value, and there may be multiple real values and keys for each composite ... switch (op) { case EQUAL_TO: T lowerValue = converter.toLowerValue(operand, variables); T upperValue = converter.toUpperValue(operand, variables); return create(keysByValue.subMap(lowerValue, true, upperValue, true)); case GREATER_THAN: T value = converter.toUpperValue(operand, variables); return create(keysByValue.tailMap(value, false)); case GREATER_THAN_OR_EQUAL_TO: value = converter.toLowerValue(operand, variables); return create(keysByValue.tailMap(value, true)); case LESS_THAN: value = converter.toLowerValue(operand, variables); return create(keysByValue.headMap(value, false)); case LESS_THAN_OR_EQUAL_TO: value = converter.toUpperValue(operand, variables); return create(keysByValue.headMap(value, true)); case NOT_EQUAL_TO: OperationBuilder<T> lowerOp = create(keysByValue.headMap(converter.toLowerValue(operand, variables), false)); OperationBuilder<T> upperOp = create(keysByValue.tailMap(converter.toUpperValue(operand, variables), false)); return new DualOperationBuilder<>(lowerOp, upperOp); case LIKE: // We can't handle LIKE with this kind of index, but we can return the complete list of node keys // that have properties matching this index, and the LIKE can be done higher up. This might not very useful, // but more than likely we're going to know the subset of nodes with this property better than any other // index except somethiing that can actually handle LIKE. So we won't filter, and we'll just return this // builder... break; } return this; } @Override protected OperationBuilder<T> apply( SetCriteria setCriteria, boolean negated ) { return new SetOperationBuilder<>(keysByValue, converter, nodeKeysAccessor, variables, setCriteria, negated); } protected Iterator<String> keys() { return nodeKeysAccessor.getNodeKeys(keysByValue); } @Override public Results getResults() { final Iterator<String> filteredKeys = keys(); final float score = 1.0f; return new Results() { @Override public Filter.ResultBatch getNextBatch(int batchSize) { int count = 0; final LinkedHashMap<NodeKey, Float> keysByScore = new LinkedHashMap<>(); while (count < batchSize && filteredKeys.hasNext()) { keysByScore.put(new NodeKey(filteredKeys.next()), score); count++; } return new Filter.ResultBatch() { @Override public Iterable<NodeKey> keys() { return () -> keysByScore.keySet().iterator(); } @Override public Iterable<Float> scores() { return () -> keysByScore.values().iterator(); } @Override public boolean hasNext() { return filteredKeys.hasNext(); } @Override public int size() { return keysByScore.size(); } }; } @Override public void close() { // Nothing to do ... } }; } @Override public long estimateCount() { return keysByValue.size(); } } /** * This builder results in an operation that performs a positive or negative match against a set criteria containing known * values. * * @param <T> the type of index key * @param <V> the type of value to be iterated over * @author Randall Hauch (rhauch@redhat.com) */ protected static class SetOperationBuilder<T, V> extends BasicOperationBuilder<T, V> { private final SetCriteria criteria; private final boolean negated; protected SetOperationBuilder( NavigableMap<T, V> keysByValue, IndexValues.Converter<T> converter, NodeKeysAccessor<T, V> nodeKeysAccessor, Map<String, Object> variables, SetCriteria criteria, boolean negated ) { super(keysByValue, converter, nodeKeysAccessor, variables); this.criteria = criteria; this.negated = negated; } @Override protected OperationBuilder<T> create( NavigableMap<T, V> keysByValue ) { return new SetOperationBuilder<>(keysByValue, converter, nodeKeysAccessor, variables, criteria, negated); } @Override protected OperationBuilder<T> apply( SetCriteria setCriteria, boolean negated ) { throw new UnsupportedOperationException("Can't evaluate two SetCriteria that are not ANDed or ORed together"); } private void addValues( StaticOperand valueOperand, Set<String> matchedKeys ) { if (valueOperand instanceof BindVariableName) { // We have to resolve the variable ... BindVariableName varName = (BindVariableName)valueOperand; String varNameStr = varName.getBindVariableName(); Object varValue = this.variables.get(varNameStr); if (varValue instanceof Collection) { Collection<?> collection = (Collection<?>)varValue; for (Object value : collection) { StaticOperand operand = new Literal(value); addValues(operand, matchedKeys); } } else { StaticOperand operand = new Literal(varValue); addValues(operand, matchedKeys); } // Not a value we know what to do with ... return; } T lowValue = converter.toLowerValue(valueOperand, variables); T highValue = converter.toUpperValue(valueOperand, variables); NavigableMap<T, V> submap = null; if (lowValue == null) { if (highValue == null) return; // High but not low ... submap = keysByValue.headMap(highValue, true); } else { if (highValue == null) { // Low but not high ... submap = keysByValue.tailMap(lowValue, true); } else { // Both high and low ... submap = keysByValue.subMap(lowValue, true, highValue, true); } } if (submap.isEmpty()) return; // no values for these keys nodeKeysAccessor.addAllTo(submap, matchedKeys); } @Override protected Iterator<String> keys() { // Determine the set of keys that have a value in our set ... final Set<String> matchedKeys = new HashSet<>(); for (StaticOperand valueOperand : criteria.getValues()) { // Find the range of all keys that have this value ... addValues(valueOperand, matchedKeys); } if (!negated) { // We've already found all of the keys for the given value, so just return them ... return matchedKeys.iterator(); } // Otherwise, we're supposed to find all of the keys that are NOT in the set ... final Iterator<String> allKeys = super.keys(); if (matchedKeys.isEmpty()) { // Our key set is empty, so none are to be excluded... return allKeys; } // Otherwise, we'll return an iterator that just wraps the 'allKeys' iterator and skips any key in our set ... return new Iterator<String>() { private String next; @Override public boolean hasNext() { return findNext(); } @Override public String next() { if (findNext()) { assert next != null; String result = next; next = null; return result; } throw new NoSuchElementException(); } @Override public void remove() { throw new UnsupportedOperationException(); } private boolean findNext() { if (next != null) return true; while (allKeys.hasNext()) { String possible = allKeys.next(); if (matchedKeys.contains(possible)) continue; // it is to be excluded next = possible; return true; } return false; } }; } @Override public long estimateCount() { long count = 0L; // Determine the set of keys that have a value in our set ... for (StaticOperand valueOperand : criteria.getValues()) { // Find the range of all keys that have this value ... T lowValue = converter.toLowerValue(valueOperand, variables); T highValue = converter.toUpperValue(valueOperand, variables); NavigableMap<T, V> submap = null; if (lowValue == null) { if (highValue == null) continue; // High but not low ... submap = keysByValue.headMap(highValue, true); } else { if (highValue == null) { // Low but not high ... submap = keysByValue.tailMap(lowValue, true); } else { // Both high and low ... submap = keysByValue.subMap(lowValue, true, highValue, true); } } count += submap.size(); } if (!negated) { // We've already found all of the keys for the given value, so just return them ... count = keysByValue.size() - count; } return Math.max(count, 0L); } } /** * This builder delegates to both sides, and is used for {@link Or} constraints. * * @param <T> the type of index key * @author Randall Hauch (rhauch@redhat.com) */ protected static class DualOperationBuilder<T> extends OperationBuilder<T> { protected final OperationBuilder<T> left; protected final OperationBuilder<T> right; protected DualOperationBuilder( OperationBuilder<T> left, OperationBuilder<T> right ) { this.left = left; this.right = right; } @Override protected OperationBuilder<T> apply( Between between, boolean negated ) { OperationBuilder<T> left = this.left.apply(between, negated); OperationBuilder<T> right = this.right.apply(between, negated); return new DualOperationBuilder<>(left, right); } @Override protected OperationBuilder<T> apply( Comparison comparison, boolean negated ) { OperationBuilder<T> left = this.left.apply(comparison, negated); OperationBuilder<T> right = this.right.apply(comparison, negated); return new DualOperationBuilder<>(left, right); } @Override protected OperationBuilder<T> apply( SetCriteria setCriteria, boolean negated ) { OperationBuilder<T> left = this.left.apply(setCriteria, negated); OperationBuilder<T> right = this.right.apply(setCriteria, negated); return new DualOperationBuilder<>(left, right); } @Override public Results getResults() { final Results first = left.getResults(); return new Results() { Results second = null; Results first = null; @Override public Filter.ResultBatch getNextBatch(int batchSize) { if (second == null) { if (first == null) { first = left.getResults(); } Filter.ResultBatch resultFromFirst = first.getNextBatch(batchSize); if (resultFromFirst.hasNext()) { return resultFromFirst; } // no more batches in the 1st result set so we need to look at the second first = null; second = right.getResults(); if (resultFromFirst.size() > 0) { return new Filter.ResultBatch() { @Override public Iterable<NodeKey> keys() { return resultFromFirst.keys(); } @Override public Iterable<Float> scores() { return resultFromFirst.scores(); } @Override public boolean hasNext() { return second != null; } @Override public int size() { return resultFromFirst.size(); } }; } } return second.getNextBatch(batchSize); } @Override public void close() { // Nothing to do ... } }; } @Override public long estimateCount() { return left.estimateCount() + right.estimateCount(); } } private Operations() { } }