Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java (revision 1575879) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java (working copy) @@ -30,6 +30,7 @@ import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.UNIQUE_PROPERTY_NAME; import java.util.Collection; +import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -128,16 +129,8 @@ @Nonnull String[] propertyNames, @Nullable String[] declaringNodeTypeNames, @Nonnull String propertyIndexType) throws RepositoryException { - NodeUtil entry = indexNode.getOrAddChild(indexDefName, INDEX_DEFINITIONS_NODE_TYPE); - entry.setString(TYPE_PROPERTY_NAME, propertyIndexType); - entry.setBoolean(REINDEX_PROPERTY_NAME, true); - if (unique) { - entry.setBoolean(UNIQUE_PROPERTY_NAME, true); - } - if (declaringNodeTypeNames != null && declaringNodeTypeNames.length > 0) { - entry.setNames(DECLARING_NODE_TYPES, declaringNodeTypeNames); - } - entry.setNames(PROPERTY_NAMES, propertyNames); + createIndexDefinition(indexNode, indexDefName, unique, propertyNames, + declaringNodeTypeNames, propertyIndexType, null); } public static void createReferenceIndex(@Nonnull NodeBuilder index) { @@ -161,4 +154,41 @@ && type.getValue(Type.STRING).equals(typeIn); } + /** + * Create a new property index definition below the given {@code indexNode} of the provided + * {@code propertyIndexType}. + * + * @param indexNode + * @param indexDefName + * @param unique + * @param propertyNames + * @param declaringNodeTypeNames + * @param propertyIndexType + * @param properties any additional property to be added to the index definition. + * @throws RepositoryException + */ + public static void createIndexDefinition(@Nonnull NodeUtil indexNode, + @Nonnull String indexDefName, + boolean unique, + @Nonnull String[] propertyNames, + @Nullable String[] declaringNodeTypeNames, + @Nonnull String propertyIndexType, + Map properties) throws RepositoryException { + NodeUtil entry = indexNode.getOrAddChild(indexDefName, INDEX_DEFINITIONS_NODE_TYPE); + entry.setString(TYPE_PROPERTY_NAME, propertyIndexType); + entry.setBoolean(REINDEX_PROPERTY_NAME, true); + if (unique) { + entry.setBoolean(UNIQUE_PROPERTY_NAME, true); + } + if (declaringNodeTypeNames != null && declaringNodeTypeNames.length > 0) { + entry.setNames(DECLARING_NODE_TYPES, declaringNodeTypeNames); + } + entry.setNames(PROPERTY_NAMES, propertyNames); + + if (properties != null) { + for (String k : properties.keySet()) { + entry.setString(k, properties.get(k)); + } + } + } } Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java (revision 1575879) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java (working copy) @@ -17,10 +17,65 @@ package org.apache.jackrabbit.oak.plugins.index.property; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * interface for shared constants around different actors: QueryIndex, IndexEditors, * IndexEditorProviders, ... */ public interface OrderedIndex { + /** + * enum for easing the order direction of the index + */ + enum OrderDirection { + /** + * ascending order configuration (default) + */ + ASC("ascending"), + + /** + * descending order configuration + */ + DESC("descending"); + + private final String direction; + private OrderDirection(String direction) { + this.direction = direction; + } + public String getDirection() { + return direction; + } + + /** + * retrieve an {@code OrderDirection} from a provided String. Will return null in case of no-match + * + * @param direction the direction of the sorting: ascending or descending + * @return the order + */ + @Nullable @CheckForNull + public static OrderDirection fromString(@Nonnull final String direction) { + for (OrderDirection d : OrderDirection.values()) { + if (d.getDirection().equalsIgnoreCase(direction)) { + return d; + } + } + return null; + } + }; + String TYPE = "ordered"; + + /** + * the 'key' used for specifying the direction of the index when providing the configuration + * + * {@code "propertyNames"="foobar", "direction"="ascending" } + */ + String DIRECTION = "direction"; + + /** + * the default direction for sorting the index + */ + OrderDirection DEFAULT_DIRECTION = OrderDirection.ASC; } Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java (revision 1575879) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java (working copy) @@ -19,10 +19,16 @@ import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.TYPE; +import org.apache.jackrabbit.oak.spi.query.Filter; import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * A property index that supports ordering keys. + */ public class OrderedPropertyIndex extends PropertyIndex { - + private static final Logger LOG = LoggerFactory.getLogger(OrderedPropertyIndex.class); @Override public String getIndexName() { return TYPE; @@ -32,4 +38,11 @@ PropertyIndexLookup getLookup(NodeState root) { return new OrderedPropertyIndexLookup(root); } + + @Override + public double getCost(Filter filter, NodeState root) { + //we don't want the index to be used yet + LOG.warn("this index will always return Double.POSITIVE_INFINITY and therefore never work"); + return Double.POSITIVE_INFINITY; + } } Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java (revision 1575879) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java (working copy) @@ -22,14 +22,13 @@ import javax.annotation.Nonnull; -import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.IndexConstants; import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy; import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy; -import org.apache.jackrabbit.oak.spi.commit.Editor; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; import org.slf4j.Logger; @@ -37,40 +36,68 @@ import com.google.common.base.Strings; +/** + * Index editor for keeping an ordered property index up to date. + */ public class OrderedPropertyIndexEditor extends PropertyIndexEditor { - private static final Logger log = LoggerFactory.getLogger(OrderedPropertyIndexEditor.class); - private static final IndexStoreStrategy ORDERED_MIRROR = new OrderedContentMirrorStoreStrategy(); + /** + * the default Ascending ordered StoreStrategy + */ + static final IndexStoreStrategy ORDERED_MIRROR = new OrderedContentMirrorStoreStrategy(); + + /** + * the Descending ordered StoreStrategy + */ + static final IndexStoreStrategy ORDERED_MIRROR_DESCENDING = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + private static final Logger LOG = LoggerFactory.getLogger(OrderedPropertyIndexEditor.class); + private final Set propertyNames; private boolean properlyConfigured; + private OrderDirection direction = OrderedIndex.DEFAULT_DIRECTION; + public OrderedPropertyIndexEditor(NodeBuilder definition, NodeState root, IndexUpdateCallback callback) { super(definition, root, callback); + // configuring propertyNames Set pns = null; - PropertyState names = definition.getProperty(IndexConstants.PROPERTY_NAMES); if (names != null) { String value = names.getValue(Type.NAME, 0); if (Strings.isNullOrEmpty(value)) { - log.warn("Empty value passed as propertyNames. Index not properly configured. Ignoring."); + LOG.warn("Empty value passed as propertyNames. Index not properly configured. Ignoring."); } else { if (names.isArray()) { - log.warn("Only single value supported. '{}' only will be used.", value); + LOG.warn("Only single value supported. '{}' only will be used.", value); } pns = Collections.singleton(value); this.properlyConfigured = true; } } - this.propertyNames = pns; + + // configuring direction + String propertyDirection = definition.getString(OrderedIndex.DIRECTION); + if (propertyDirection == null) { + LOG.info("Using default direction for sorting: {}", this.direction); + } else { + OrderDirection dir = OrderDirection.fromString(propertyDirection); + if (dir == null) { + LOG.warn("An unknown direction has been specified for sorting: '{}'. Using default one. {}", + propertyDirection, this.direction); + } else { + this.direction = dir; + } + } } OrderedPropertyIndexEditor(OrderedPropertyIndexEditor parent, String name) { super(parent, name); this.propertyNames = parent.getPropertyNames(); + this.direction = parent.getDirection(); } /** @@ -80,7 +107,11 @@ */ @Override IndexStoreStrategy getStrategy(boolean unique) { - return ORDERED_MIRROR; + IndexStoreStrategy store = ORDERED_MIRROR; + if (!OrderedIndex.DEFAULT_DIRECTION.equals(getDirection())) { + store = ORDERED_MIRROR_DESCENDING; + } + return store; } public boolean isProperlyConfigured() { @@ -98,53 +129,11 @@ return new OrderedPropertyIndexEditor(this, name); } - @Override - public void enter(NodeState before, NodeState after) { - log.debug("enter() - before: {} - after: {}", before, after); - super.enter(before, after); - } - - @Override - public void leave(NodeState before, NodeState after) throws CommitFailedException { - log.debug("leave() - before: {} - after: {}", before, after); - super.leave(before, after); - } - - @Override - public void propertyAdded(PropertyState after) { - log.debug("propertyAdded() - after: {}", after); - super.propertyAdded(after); - } - - @Override - public void propertyChanged(PropertyState before, PropertyState after) { - log.debug("propertyChanged() - before: {} - after: {}", before, after); - super.propertyChanged(before, after); - } - - @Override - public void propertyDeleted(PropertyState before) { - log.debug("propertyDeleted() - before: {}", before); - super.propertyDeleted(before); - } - - @Override - public Editor childNodeAdded(String name, NodeState after) { - log.debug("childNodeAdded() - name: {} - after: {}", name, after); - return super.childNodeAdded(name, after); - } - - @Override - public Editor childNodeChanged(String name, NodeState before, NodeState after) { - log.debug("childNodeChanged() - name: {} - before: {} - after: {}", new Object[] { name, - before, - after }); - return super.childNodeChanged(name, before, after); - } - - @Override - public Editor childNodeDeleted(String name, NodeState before) { - log.debug("childNodeDeleted() - name: {} - before: {}", name, before); - return super.childNodeDeleted(name, before); + /** + * + * @return the direction of the index configuration + */ + public OrderDirection getDirection() { + return direction; } } Index: src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java (revision 1575879) +++ src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java (working copy) @@ -26,6 +26,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry; import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; @@ -51,7 +53,6 @@ * */ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrategy { - private static final Logger log = LoggerFactory.getLogger(OrderedContentMirrorStoreStrategy.class); /** * the property linking to the next node @@ -70,9 +71,25 @@ .setProperty(NEXT, "") .getNodeState(); + private static final Logger LOG = LoggerFactory.getLogger(OrderedContentMirrorStoreStrategy.class); + + /** + * the direction of the index. + */ + private OrderDirection direction = OrderedIndex.DEFAULT_DIRECTION; + + public OrderedContentMirrorStoreStrategy() { + super(); + } + + public OrderedContentMirrorStoreStrategy(OrderDirection direction) { + this(); + this.direction = direction; + } + @Override NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index, @Nonnull String key) { - log.debug("fetchKeyNode() - index: {} - key: {}", index, key); + LOG.debug("fetchKeyNode() - index: {} - key: {}", index, key); NodeBuilder localkey = null; NodeBuilder start = index.child(START); @@ -86,26 +103,21 @@ } else { // specific use-case where the item has to be added as first of the list String nextKey = n; - if (key.compareTo(nextKey) < 0) { - localkey = index.child(key); - localkey.setProperty(NEXT, nextKey); - start.setProperty(NEXT, key); - } else { - Iterable children = getChildNodeEntries(index.getNodeState()); - for (ChildNodeEntry child : children) { - nextKey = child.getNodeState().getString(NEXT); - if (Strings.isNullOrEmpty(nextKey)) { - // we're at the last element, therefore our 'key' has to be appended + Iterable children = getChildNodeEntries(index.getNodeState(), + true); + for (ChildNodeEntry child : children) { + nextKey = child.getNodeState().getString(NEXT); + if (Strings.isNullOrEmpty(nextKey)) { + // we're at the last element, therefore our 'key' has to be appended + index.getChildNode(child.getName()).setProperty(NEXT, key); + localkey = index.child(key); + localkey.setProperty(NEXT, ""); + } else { + if (isInsertHere(key, nextKey)) { index.getChildNode(child.getName()).setProperty(NEXT, key); localkey = index.child(key); - localkey.setProperty(NEXT, ""); - } else { - if (key.compareTo(nextKey) < 0) { - index.getChildNode(child.getName()).setProperty(NEXT, key); - localkey = index.child(key); - localkey.setProperty(NEXT, nextKey); - break; - } + localkey.setProperty(NEXT, nextKey); + break; } } } @@ -114,6 +126,21 @@ return localkey; } + /** + * tells whether or not the is right to insert here a new item. + * + * @param newItemKey the new item key to be added + * @param existingItemKey the 'here' of the existing index + * @return true for green light on insert false otherwise. + */ + private boolean isInsertHere(@Nonnull String newItemKey, @Nonnull String existingItemKey) { + if (OrderDirection.ASC.equals(direction)) { + return newItemKey.compareTo(existingItemKey) < 0; + } else { + return newItemKey.compareTo(existingItemKey) > 0; + } + } + @Override void prune(final NodeBuilder index, final Deque builders) { for (NodeBuilder node : builders) { @@ -122,11 +149,12 @@ } else if (node.exists()) { if (node.hasProperty(NEXT)) { // it's an index key and we have to relink the list - ChildNodeEntry previous = findPrevious(index.getNodeState(), - node.getNodeState()); // (1) find the - // previous element - log.debug("previous: {}", previous); - String next = node.getString(NEXT); // (2) find the next element + // (1) find the previous element + ChildNodeEntry previous = findPrevious( + index.getNodeState(), node.getNodeState()); + LOG.debug("previous: {}", previous); + // (2) find the next element + String next = node.getString(NEXT); if (next == null) { next = ""; } @@ -138,6 +166,20 @@ } } + /** + * find the previous item (ChildNodeEntry) in the index given the provided NodeState for + * comparison + * + * in an index sorted in ascending manner where we have @{code [1, 2, 3, 4, 5]} if we ask for + * a previous given 4 it will be 3. previous(4)=3. + * + * in an index sorted in descending manner where we have @{code [5, 4, 3, 2, 1]} if we as for + * a previous given 4 it will be 5. previous(4)=5. + * + * @param index the index we want to look into ({@code :index}) + * @param node the node we want to compare + * @return the previous item or null if not found. + */ @Nullable ChildNodeEntry findPrevious(@Nonnull final NodeState index, @Nonnull final NodeState node) { ChildNodeEntry previous = null; @@ -158,16 +200,16 @@ } } - return (found) ? previous : null; + return found ? previous : null; } @Override public void update(NodeBuilder index, String path, Set beforeKeys, Set afterKeys) { - log.debug("update() - index : {}", index); - log.debug("update() - path : {}", path); - log.debug("update() - beforeKeys: {}", beforeKeys); - log.debug("update() - afterKeys : {}", afterKeys); + LOG.debug("update() - index : {}", index); + LOG.debug("update() - path : {}", path); + LOG.debug("update() - beforeKeys: {}", beforeKeys); + LOG.debug("update() - afterKeys : {}", afterKeys); super.update(index, path, beforeKeys, afterKeys); } @@ -203,8 +245,8 @@ } else { cne = new Iterable() { private NodeState localIndex = index; - private NodeState localStart = ((includeStart && !start.exists()) ? EMPTY_START_NODE - : start); + private NodeState localStart = includeStart && !start.exists() ? EMPTY_START_NODE + : start; private NodeState current = localStart; private boolean localIncludeStart = includeStart; @@ -214,7 +256,7 @@ @Override public boolean hasNext() { - return ((localIncludeStart && localStart.equals(current)) || (!localIncludeStart && !Strings.isNullOrEmpty(current.getString(NEXT)))); + return (localIncludeStart && localStart.equals(current)) || (!localIncludeStart && !Strings.isNullOrEmpty(current.getString(NEXT))); } @Override @@ -270,4 +312,4 @@ return state; } } -} +} \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java (revision 0) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java (working copy) @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.index.property; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.api.ContentRepository; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; +import org.apache.jackrabbit.oak.query.AbstractQueryTest; +import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; + +public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTest { + /** + * the property used by the index + */ + public static final String ORDERED_PROPERTY = "foo"; + + /** + * number of nodes to create for testing. + * + * It has been found during development that in some cases the order of the nodes creation + * within the persistence where the actual expected order. + * + * The higher the value the lower the chance for this to happen. + */ + protected static final int NUMBER_OF_NODES = 50; + + /** + * generate a list of values to be used as ordered set. Will return something like + * {@code value000, value001, value002, ...} + * + * + * @param amount + * @param direction the direction of the sorting + * @return a list of {@code amount} values ordered as specified by {@code direction} + */ + protected static List generateOrderedValues(int amount, OrderDirection direction) { + if (amount > 1000) { + throw new RuntimeException("amount cannot be greater than 1000"); + } + List values = new ArrayList(amount); + NumberFormat nf = new DecimalFormat("000"); + + if (OrderDirection.DESC.equals(direction)) { + for (int i = amount; i > 0; i--) { + values.add(String.format("value%s", String.valueOf(nf.format(i)))); + } + } else { + for (int i = 0; i < amount; i++) { + values.add(String.format("value%s", String.valueOf(nf.format(i)))); + } + } + return values; + } + + /** + * as {@code generateOrderedValues(int, OrderDirection)} by forcing OrderDirection.ASC + * + * @param amount + * @return + */ + protected static List generateOrderedValues(int amount) { + return generateOrderedValues(amount, OrderDirection.ASC); + } + + /** + * create a child node for the provided father + * + * @param father + * @param name + * the name of the node to create + * @param propName + * the name of the property to assign + * @param propValue + * the value of the property to assign + * @return + */ + static Tree child(Tree father, String name, String propName, String propValue) { + Tree child = father.addChild(name); + child.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME); + child.setProperty(propName, propValue, Type.STRING); + return child; + } + + @Override + protected ContentRepository createRepository() { + return new Oak().with(new InitialContent()) + .with(new OpenSecurityProvider()) + .with(new LowCostOrderedPropertyIndexProvider()) + .with(new OrderedPropertyIndexEditorProvider()) + .createContentRepository(); + } + + /** + * convenience method that adds a bunch of nodes in random order and return the order in which + * they should be presented by the OrderedIndex + * + * @param values the values of the property that will be indexed + * @param father the father under which add the nodes + * @param direction the direction of the items to be added. + * @return + */ + protected List addChildNodes(final List values, final Tree father, + OrderDirection direction) { + List nodes = new ArrayList(); + Random rnd = new Random(); + int counter = 0; + while (!values.isEmpty()) { + String v = values.remove(rnd.nextInt(values.size())); + Tree t = child(father, String.format("n%s", counter++), ORDERED_PROPERTY, v); + nodes.add(new ValuePathTuple(v, t.getPath())); + } + + if (OrderDirection.DESC.equals(direction)) { + Collections.sort(nodes, Collections.reverseOrder()); + } else { + Collections.sort(nodes); + } + return nodes; + } + + /** + * assert the right order of the returned resultset + * + * @param orderedSequence the right order in which the resultset should be returned + * @param resultset the resultset + */ + protected void assertRightOrder(@Nonnull final List orderedSequence, + @Nonnull final Iterator resultset) { + assertTrue("No results returned", resultset.hasNext()); + int counter = 0; + while (resultset.hasNext() && counter < orderedSequence.size()) { + ResultRow row = resultset.next(); + assertEquals( + String.format("Wrong path at the element '%d'", counter), + orderedSequence.get(counter).getPath(), + row.getPath() + ); + counter++; + } + } +} Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java (revision 0) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java (working copy) @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.index.property; + +import java.util.List; + +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.ImmutableList; + +/** + * convenience provider for going around the INFINITE cost of the actual index implementation + */ +public class LowCostOrderedPropertyIndexProvider extends OrderedPropertyIndexProvider { + @Override + public List getQueryIndexes(NodeState nodeState) { + return ImmutableList. of(new LowCostOrderedPropertyIndex()); + } + + private static class LowCostOrderedPropertyIndex extends OrderedPropertyIndex { + @Override + public double getCost(Filter filter, NodeState root) { + return 1e-3; + } + } +} Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java (revision 0) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java (working copy) @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.index.property; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; + +import java.text.ParseException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.ResultRow; +import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.IndexUtils; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.util.NodeUtil; +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; + + +public class OrderedPropertyIndexDescendingQueryTest extends BasicOrderedPropertyIndexQueryTest { + @Override + protected void createTestIndexNode() throws Exception { + Tree index = root.getTree("/"); + IndexUtils.createIndexDefinition( + new NodeUtil(index.getChild(IndexConstants.INDEX_DEFINITIONS_NAME)), + TEST_INDEX_NAME, + false, + new String[] { ORDERED_PROPERTY }, + null, + OrderedIndex.TYPE, + ImmutableMap.of( + OrderedIndex.DIRECTION, OrderedIndex.OrderDirection.DESC.getDirection() + ) + ); + root.commit(); + } + + /** + * Query the index for retrieving all the entries + * + * @throws CommitFailedException + * @throws ParseException + * @throws RepositoryException + */ + @Test + public void queryAllEntries() throws CommitFailedException, ParseException, RepositoryException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + List nodes = addChildNodes( + generateOrderedValues(NUMBER_OF_NODES, OrderDirection.DESC), test, OrderDirection.DESC); + root.commit(); + + // querying + Iterator results; + results = executeQuery( + String.format("SELECT * from [%s] WHERE foo IS NOT NULL", NT_UNSTRUCTURED), SQL2, null) + .getRows().iterator(); + assertRightOrder(nodes, results); + + setTravesalEnabled(true); + } + + /** + * test the index for returning the items related to a single key + * + * @throws CommitFailedException + * @throws ParseException + */ + @Test + public void queryOneKey() throws CommitFailedException, ParseException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.DESC); + root.commit(); + + // getting the middle of the random list of nodes + ValuePathTuple searchfor = nodes.get(NUMBER_OF_NODES / 2); + + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newString(searchfor.getValue())); + String query = "SELECT * FROM [%s] WHERE %s=$%s"; + Iterator results = executeQuery( + String.format(query, NT_UNSTRUCTURED, ORDERED_PROPERTY, ORDERED_PROPERTY), + SQL2, + filter).getRows().iterator(); + assertTrue("one element is expected", results.hasNext()); + assertEquals("wrong path returned", searchfor.getPath(), results.next().getPath()); + assertFalse("there should be not any more items", results.hasNext()); + + setTravesalEnabled(true); + } +} Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java (revision 1575879) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java (working copy) @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.jackrabbit.oak.plugins.index.property; import static org.easymock.EasyMock.createNiceMock; @@ -23,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; @@ -30,12 +30,17 @@ import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.junit.Test; +/** + * Tests the ordered index. + */ public class OrderedPropertyIndexEditorTest { - @Test public void isProperlyConfiguredWithPropertyNames(){ + @Test public void isProperlyConfiguredWithPropertyNames() { NodeBuilder definition = createNiceMock(NodeBuilder.class); PropertyState names = createNiceMock(PropertyState.class); expect(names.count()).andReturn(1); @@ -44,41 +49,101 @@ replay(definition); OrderedPropertyIndexEditor ie = new OrderedPropertyIndexEditor(definition, null, null); - assertFalse("With empty or missing property the index should not work.",ie.isProperlyConfigured()); + assertFalse("With empty or missing property the index should not work.", ie.isProperlyConfigured()); } - @Test public void isProperlyConfiguredSingleValuePropertyNames(){ + @Test public void isProperlyConfiguredSingleValuePropertyNames() { NodeBuilder definition = createNiceMock(NodeBuilder.class); PropertyState names = createNiceMock(PropertyState.class); expect(names.count()).andReturn(1); - expect(names.getValue(Type.NAME,0)).andReturn("jcr:lastModified").anyTimes(); + expect(names.getValue(Type.NAME, 0)).andReturn("jcr:lastModified").anyTimes(); expect(definition.getProperty(IndexConstants.PROPERTY_NAMES)).andReturn(names).anyTimes(); replay(names); replay(definition); OrderedPropertyIndexEditor ie = new OrderedPropertyIndexEditor(definition, null, null); - assertNotNull("With a correct property set 'propertyNames' can't be null",ie.getPropertyNames()); - assertEquals(1,ie.getPropertyNames().size()); - assertEquals("jcr:lastModified",ie.getPropertyNames().iterator().next()); - assertTrue("Expecting a properly configured index",ie.isProperlyConfigured()); + assertNotNull("With a correct property set 'propertyNames' can't be null", ie.getPropertyNames()); + assertEquals(1, ie.getPropertyNames().size()); + assertEquals("jcr:lastModified", ie.getPropertyNames().iterator().next()); + assertTrue("Expecting a properly configured index", ie.isProperlyConfigured()); } - @Test public void multiValueProperty(){ + @Test public void multiValueProperty() { NodeBuilder definition = createNiceMock(NodeBuilder.class); PropertyState names = createNiceMock(PropertyState.class); expect(names.isArray()).andReturn(true).anyTimes(); expect(names.count()).andReturn(2).anyTimes(); - expect(names.getValue(Type.NAME,0)).andReturn("jcr:lastModified").anyTimes(); - expect(names.getValue(Type.NAME,1)).andReturn("foo:bar").anyTimes(); - expect(names.getValue(Type.NAMES)).andReturn(Arrays.asList("jcr:lastModified","foo:bar")).anyTimes(); + expect(names.getValue(Type.NAME, 0)).andReturn("jcr:lastModified").anyTimes(); + expect(names.getValue(Type.NAME, 1)).andReturn("foo:bar").anyTimes(); + expect(names.getValue(Type.NAMES)).andReturn(Arrays.asList("jcr:lastModified", "foo:bar")).anyTimes(); expect(definition.getProperty(IndexConstants.PROPERTY_NAMES)).andReturn(names).anyTimes(); replay(names); replay(definition); OrderedPropertyIndexEditor ie = new OrderedPropertyIndexEditor(definition, null, null); - assertNotNull("With a correct property set 'propertyNames' can't be null",ie.getPropertyNames()); - assertEquals("When multiple properties are a passed only the first one is taken", 1,ie.getPropertyNames().size()); - assertEquals("jcr:lastModified",ie.getPropertyNames().iterator().next()); - assertTrue("Expecting a properly configured index",ie.isProperlyConfigured()); - } -} + assertNotNull("With a correct property set 'propertyNames' can't be null", ie.getPropertyNames()); + assertEquals("When multiple properties are a passed only the first one is taken", 1, ie.getPropertyNames().size()); + assertEquals("jcr:lastModified", ie.getPropertyNames().iterator().next()); + assertTrue("Expecting a properly configured index", ie.isProperlyConfigured()); + } + + @Test + public void orderedDirectionFromString() { + assertNull("A non-existing order direction should result in null", OrderDirection.fromString("foobar")); + assertEquals(OrderDirection.ASC, OrderDirection.fromString("ascending")); + assertFalse(OrderDirection.ASC.equals(OrderDirection.fromString("descending"))); + assertEquals(OrderDirection.DESC, OrderDirection.fromString("descending")); + assertFalse(OrderDirection.DESC.equals(OrderDirection.fromString("ascending"))); + } + + @Test + public void orderDirectionDefinitionNotSpecified() { + final String property = "foobar"; + NodeBuilder definition = EmptyNodeState.EMPTY_NODE.builder(); + definition.setProperty(IndexConstants.PROPERTY_NAMES, property); + OrderedPropertyIndexEditor editor = new OrderedPropertyIndexEditor(definition, null, null); + assertNotNull(editor.getPropertyNames()); + assertEquals(property, editor.getPropertyNames().iterator().next()); + assertEquals(OrderedIndex.OrderDirection.ASC, editor.getDirection()); + } + + @Test + public void orderDirectionDefinitionDescending() { + final String property = "foobar"; + NodeBuilder definition = EmptyNodeState.EMPTY_NODE.builder(); + definition.setProperty(IndexConstants.PROPERTY_NAMES, property); + definition.setProperty(OrderedIndex.DIRECTION, "descending"); + OrderedPropertyIndexEditor editor = new OrderedPropertyIndexEditor(definition, null, null); + assertNotNull(editor.getPropertyNames()); + assertEquals(property, editor.getPropertyNames().iterator().next()); + assertEquals(OrderedIndex.OrderDirection.DESC, editor.getDirection()); + } + + @Test + public void orderDirectionUnknownDefinition() { + final String property = "foobar"; + NodeBuilder definition = EmptyNodeState.EMPTY_NODE.builder(); + definition.setProperty(IndexConstants.PROPERTY_NAMES, property); + definition.setProperty(OrderedIndex.DIRECTION, "bazbaz"); + OrderedPropertyIndexEditor editor = new OrderedPropertyIndexEditor(definition, null, null); + assertNotNull(editor.getPropertyNames()); + assertEquals(property, editor.getPropertyNames().iterator().next()); + assertEquals("if we provide a non-valid definition for order the Ascending is expected", OrderedIndex.OrderDirection.ASC, editor.getDirection()); + } + + @Test + public void strategies() { + final String property = "foobar"; + NodeBuilder definition = EmptyNodeState.EMPTY_NODE.builder(); + definition.setProperty(IndexConstants.PROPERTY_NAMES, property); + definition.setProperty(OrderedIndex.DIRECTION, OrderDirection.ASC.getDirection()); + OrderedPropertyIndexEditor editor = new OrderedPropertyIndexEditor(definition, null, null); + assertEquals(OrderedPropertyIndexEditor.ORDERED_MIRROR, editor.getStrategy(false)); + + definition = EmptyNodeState.EMPTY_NODE.builder(); + definition.setProperty(IndexConstants.PROPERTY_NAMES, property); + definition.setProperty(OrderedIndex.DIRECTION, OrderDirection.DESC.getDirection()); + editor = new OrderedPropertyIndexEditor(definition, null, null); + assertEquals(OrderedPropertyIndexEditor.ORDERED_MIRROR_DESCENDING, editor.getStrategy(false)); + } +} \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java (revision 1575879) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java (working copy) @@ -20,138 +20,29 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; -import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; -import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Random; -import javax.annotation.Nonnull; import javax.jcr.RepositoryException; -import org.apache.jackrabbit.oak.Oak; import org.apache.jackrabbit.oak.api.CommitFailedException; -import org.apache.jackrabbit.oak.api.ContentRepository; import org.apache.jackrabbit.oak.api.PropertyValue; import org.apache.jackrabbit.oak.api.ResultRow; import org.apache.jackrabbit.oak.api.Tree; -import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.IndexConstants; import org.apache.jackrabbit.oak.plugins.index.IndexUtils; -import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; -import org.apache.jackrabbit.oak.query.AbstractQueryTest; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.spi.query.PropertyValues; -import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; import org.apache.jackrabbit.oak.util.NodeUtil; import org.junit.Test; import com.google.common.collect.ImmutableMap; -public class OrderedPropertyIndexQueryTest extends AbstractQueryTest { - /** - * the property used by the index - */ - public static final String ORDERED_PROPERTY = "foo"; - - /** - * number of nodes to create for testing. - * - * It has been found during development that in some cases the order of the nodes creation within the persistence - * where the actual expected order. - * - * The higher the value the lower the chance for this to happen. - */ - private static final int NUMBER_OF_NODES = 50; - - /** - * convenience orderable object that represents a tuple of values and paths - * - * where the values are the indexed keys from the index and the paths are the path which hold the key - */ - private class ValuePathTuple implements Comparable { - private final String value; - private final String path; - - ValuePathTuple(String value, String path) { - this.value = value; - this.path = path; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + getOuterType().hashCode(); - result = prime * result + ((path == null) ? 0 : path.hashCode()); - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj){ - return true; - } - if (obj == null){ - return false; - } - if (getClass() != obj.getClass()){ - return false; - } - ValuePathTuple other = (ValuePathTuple) obj; - if (!getOuterType().equals(other.getOuterType())){ - return false; - } - if (path == null) { - if (other.path != null){ - return false; - } - } else if (!path.equals(other.path)){ - return false; - } - if (value == null) { - if (other.value != null){ - return false; - } - } else if (!value.equals(other.value)){ - return false; - } - return true; - } - - @Override - public int compareTo(ValuePathTuple o) { - if (this.equals(o)){ - return 0; - } - if (this.value.compareTo(o.value) < 0){ - return -1; - } - if (this.value.compareTo(o.value) > 0){ - return 1; - } - if (this.path.compareTo(o.path) < 0){ - return -1; - } - if (this.path.compareTo(o.path) > 0){ - return 1; - } - return 0; - } - - private OrderedPropertyIndexQueryTest getOuterType() { - return OrderedPropertyIndexQueryTest.this; - } - - } - +public class OrderedPropertyIndexQueryTest extends BasicOrderedPropertyIndexQueryTest { /** * testing for asserting the right comparison behaviour of the custom class */ @@ -176,79 +67,6 @@ } @Override - protected ContentRepository createRepository() { - return new Oak().with(new InitialContent()) - .with(new OpenSecurityProvider()) - // .with(new PropertyIndexProvider()) - // .with(new PropertyIndexEditorProvider()) - .with(new OrderedPropertyIndexProvider()).with(new OrderedPropertyIndexEditorProvider()) - .createContentRepository(); - } - - /** - * create a child node for the provided father - * - * @param father - * @param name - * the name of the node to create - * @param propName - * the name of the property to assign - * @param propValue - * the value of the property to assign - * @return - */ - private static Tree child(Tree father, String name, String propName, String propValue) { - Tree child = father.addChild(name); - child.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME); - child.setProperty(propName, propValue, Type.STRING); - return child; - } - - /** - * generate a list of values to be used as ordered set. Will return something like - * {@code value000, value001, value002, ...} - * - * - * @param amount - * @return - */ - private static List generateOrderedValues(int amount) { - if (amount > 1000){ - throw new RuntimeException("amount cannot be greater than 100"); - } - List values = new ArrayList(amount); - NumberFormat nf = new DecimalFormat("000"); - for (int i = 0; i < amount; i++){ - values.add(String.format("value%s", String.valueOf(nf.format(i)))); - } - return values; - } - - /** - * convenience method that adds a bunch of nodes in random order and return the order in which they should be - * presented by the OrderedIndex - * - * @param values - * the values of the property that will be indexed - * @param father - * the father under which add the nodes - * @return - */ - private List addChildNodes(final List values, final Tree father) { - List nodes = new ArrayList(); - Random rnd = new Random(); - int counter = 0; - while (!values.isEmpty()) { - String v = values.remove(rnd.nextInt(values.size())); - Tree t = child(father, String.format("n%s", counter++), ORDERED_PROPERTY, v); - nodes.add(new ValuePathTuple(v, t.getPath())); - } - - Collections.sort(nodes); - return nodes; - } - - @Override protected void createTestIndexNode() throws Exception { Tree index = root.getTree("/"); IndexUtils.createIndexDefinition(new NodeUtil(index.getChild(IndexConstants.INDEX_DEFINITIONS_NAME)), @@ -257,27 +75,6 @@ } /** - * assert the right order of the returned resultset - * - * @param orderedSequence - * the right order in which the resultset should be returned - * @param resultset - * the resultset - */ - private void assertRightOrder(@Nonnull - final List orderedSequence, @Nonnull - final Iterator resultset) { - assertTrue("No results returned", resultset.hasNext()); - int counter = 0; - while (resultset.hasNext() && counter < orderedSequence.size()) { - ResultRow row = resultset.next(); - assertEquals(String.format("Wrong path at the element '%d'", counter), orderedSequence.get(counter).path, - row.getPath()); - counter++; - } - } - - /** * Query the index for retrieving all the entries * * @throws CommitFailedException @@ -293,7 +90,7 @@ Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); - List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, OrderDirection.ASC); root.commit(); // querying @@ -320,19 +117,19 @@ Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); - List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, OrderDirection.ASC); root.commit(); - ValuePathTuple searchfor = nodes.get(NUMBER_OF_NODES / 2); // getting the middle of the random list of - // nodes. + // getting the middle of the random list of nodes. + ValuePathTuple searchfor = nodes.get(NUMBER_OF_NODES / 2); Map filter = ImmutableMap - .of(ORDERED_PROPERTY, PropertyValues.newString(searchfor.value)); + .of(ORDERED_PROPERTY, PropertyValues.newString(searchfor.getValue())); String query = "SELECT * FROM [%s] WHERE %s=$%s"; Iterator results = executeQuery( String.format(query, NT_UNSTRUCTURED, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() .iterator(); assertTrue("one element is expected", results.hasNext()); - assertEquals("wrong path returned", searchfor.path, results.next().getPath()); + assertEquals("wrong path returned", searchfor.getPath(), results.next().getPath()); assertFalse("there should be not any more items", results.hasNext()); setTravesalEnabled(true); Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java (revision 0) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java (working copy) @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.index.property; + + +/** + * convenience orderable object that represents a tuple of values and paths + * + * where the values are the indexed keys from the index and the paths are the path which hold the + * key + */ +public class ValuePathTuple implements Comparable { + private String value; + private String path; + + ValuePathTuple(String value, String path) { + this.value = value; + this.path = path; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.getClass().hashCode(); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ValuePathTuple other = (ValuePathTuple) obj; + if (path == null) { + if (other.getPath() != null) { + return false; + } + } else if (!path.equals(other.getPath())) { + return false; + } + if (value == null) { + if (other.getValue() != null) { + return false; + } + } else if (!value.equals(other.getValue())) { + return false; + } + return true; + } + + @Override + public int compareTo(ValuePathTuple o) { + if (this.equals(o)) { + return 0; + } + if (this.value.compareTo(o.getValue()) < 0) { + return -1; + } + if (this.value.compareTo(o.getValue()) > 0) { + return 1; + } + if (this.path.compareTo(o.getPath()) < 0) { + return -1; + } + if (this.path.compareTo(o.getPath()) > 0) { + return 1; + } + return 0; + } + + public String getValue() { + return value; + } + + public String getPath() { + return path; + } +} \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java (revision 1575879) +++ src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java (working copy) @@ -29,6 +29,7 @@ import java.util.Set; import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; @@ -68,29 +69,29 @@ */ @Test public void firstAndOnlyItem() { - final String PATH = "/foo/bar"; - final String[] PATH_NODES = Iterables.toArray(PathUtils.elements(PATH), String.class); - final String N0 = KEYS[0]; + final String path = "/foo/bar"; + final String[] pathNodes = Iterables.toArray(PathUtils.elements(path), String.class); + final String no = KEYS[0]; IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); NodeBuilder node = null; - store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N0)); + store.update(index, path, EMPTY_KEY_SET, newHashSet(no)); assertFalse(":index should be left alone with not changes", index.hasProperty(NEXT)); node = index.getChildNode(START); assertTrue(":index should have the :start node", node.exists()); - assertEquals(":start should point to n0", N0, node.getString(NEXT)); + assertEquals(":start should point to n0", no, node.getString(NEXT)); - node = index.getChildNode(N0); + node = index.getChildNode(no); assertTrue("n0 should exists in the index", node.exists()); assertEquals("n0 should point nowhere as it's the last (and only) element", "", node.getString(NEXT)); // checking content structure below n0 - node = node.getChildNode(PATH_NODES[0]); + node = node.getChildNode(pathNodes[0]); assertTrue("n0 should contain 'foo'", node.exists()); - node = node.getChildNode(PATH_NODES[1]); + node = node.getChildNode(pathNodes[1]); assertTrue("'foo' should contain 'bar'", node.exists()); assertTrue("the 'foo' node should have 'match=true'", node.getBoolean("match")); } @@ -110,37 +111,37 @@ */ @Test public void first2newKeysAlreadyOrdered() { - final String PATH = "/foo/bar"; - final String N0 = KEYS[0]; - final String N1 = KEYS[1]; + final String path = "/foo/bar"; + final String n0 = KEYS[0]; + final String n1 = KEYS[1]; IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); NodeBuilder node = null; - store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N0)); // first node - // arrives + // first node arrives + store.update(index, path, EMPTY_KEY_SET, newHashSet(n0)); node = index.getChildNode(START); assertTrue(":index should have :start", node.exists()); - assertEquals(":start should point to n0", N0, node.getString(NEXT)); + assertEquals(":start should point to n0", n0, node.getString(NEXT)); - node = index.getChildNode(N0); + node = index.getChildNode(n0); assertTrue(":index should have n0", node.exists()); assertEquals("n0 should point nowhere at this stage", "", node.getString(NEXT)); - store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N1)); // second node - // arrives + // second node arrives + store.update(index, path, EMPTY_KEY_SET, newHashSet(n1)); node = index.getChildNode(START); assertTrue(":index should still have :start", node.exists()); - assertEquals(":start should still point to n0", N0, node.getString(NEXT)); + assertEquals(":start should still point to n0", n0, node.getString(NEXT)); - node = index.getChildNode(N0); + node = index.getChildNode(n0); assertTrue("n0 should still exists", node.exists()); - assertEquals("n0 should point to n1", N1, node.getString(NEXT)); + assertEquals("n0 should point to n1", n1, node.getString(NEXT)); - node = index.getChildNode(N1); + node = index.getChildNode(n1); assertTrue("n1 should exists", node.exists()); assertEquals("n1 should point nowhere", "", node.getString(NEXT)); } @@ -173,17 +174,17 @@ @SuppressWarnings("unchecked") @Test public void childNodeEntriesACoupleOfMixedItems() { - final String N0 = KEYS[1]; - final String N1 = KEYS[0]; - final NodeState NODE_0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); - final NodeState NODE_1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, N0).getNodeState(); + final String n0 = KEYS[1]; + final String n1 = KEYS[0]; + final NodeState node0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + final NodeState node1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, n0).getNodeState(); OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); // setting-up the index structure - index.child(START).setProperty(NEXT, N1); - index.setChildNode(N0, NODE_0); - index.setChildNode(N1, NODE_1); + index.child(START).setProperty(NEXT, n1); + index.setChildNode(n0, node0); + index.setChildNode(n1, node1); NodeState indexState = index.getNodeState(); @@ -197,12 +198,12 @@ Iterator it = children.iterator(); assertTrue("We should have 2 elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The first element should be n1", N1, entry.getName()); - assertEquals("Wrong entry returned", NODE_1, entry.getNodeState()); + assertEquals("The first element should be n1", n1, entry.getName()); + assertEquals("Wrong entry returned", node1, entry.getNodeState()); assertTrue("We should have 1 elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The second element should be n0", N0, entry.getName()); - assertEquals("Wrong entry returned", NODE_0, entry.getNodeState()); + assertEquals("The second element should be n0", n0, entry.getName()); + assertEquals("Wrong entry returned", node0, entry.getNodeState()); assertFalse("We should have be at the end of the list", it.hasNext()); } @@ -221,17 +222,17 @@ @SuppressWarnings("unchecked") @Test public void childNodeEntriesACoupleOfMixedItemsNoStart() { - final String N0 = KEYS[1]; - final String N1 = KEYS[0]; - final NodeState NODE_0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); - final NodeState NODE_1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, N0).getNodeState(); + final String n0 = KEYS[1]; + final String n1 = KEYS[0]; + final NodeState node0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + final NodeState node1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, n0).getNodeState(); OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); // setting-up the index structure - index.child(START).setProperty(NEXT, N1); - index.setChildNode(N0, NODE_0); - index.setChildNode(N1, NODE_1); + index.child(START).setProperty(NEXT, n1); + index.setChildNode(n0, node0); + index.setChildNode(n1, node1); NodeState indexState = index.getNodeState(); @@ -245,12 +246,12 @@ Iterator it = children.iterator(); assertTrue("We should have 2 elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The first element should be n1", N1, entry.getName()); - assertEquals("Wrong entry returned", NODE_1, entry.getNodeState()); + assertEquals("The first element should be n1", n1, entry.getName()); + assertEquals("Wrong entry returned", node1, entry.getNodeState()); assertTrue("We should have 1 elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The second element should be n0", N0, entry.getName()); - assertEquals("Wrong entry returned", NODE_0, entry.getNodeState()); + assertEquals("The second element should be n0", n0, entry.getName()); + assertEquals("Wrong entry returned", node0, entry.getNodeState()); assertFalse("We should have be at the end of the list", it.hasNext()); } @@ -269,18 +270,18 @@ @SuppressWarnings("unchecked") @Test public void childNodeEntriesACoupleOfMixedItemsWithStart() { - final String N0 = KEYS[1]; - final String N1 = KEYS[0]; - final NodeState NODE_START = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, N1).getNodeState(); - final NodeState NODE_0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); - final NodeState NODE_1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, N0).getNodeState(); + final String n0 = KEYS[1]; + final String n1 = KEYS[0]; + final NodeState nodeStart = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, n1).getNodeState(); + final NodeState node0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + final NodeState node1 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, n0).getNodeState(); OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); // setting-up the index structure - index.setChildNode(START, NODE_START); - index.setChildNode(N0, NODE_0); - index.setChildNode(N1, NODE_1); + index.setChildNode(START, nodeStart); + index.setChildNode(n0, node0); + index.setChildNode(n1, node1); NodeState indexState = index.getNodeState(); @@ -295,15 +296,15 @@ assertTrue("We should still have elements left to loop through", it.hasNext()); entry = it.next(); assertEquals("The first element should be :start", START, entry.getName()); - assertEquals("Wrong entry returned", NODE_START, entry.getNodeState()); + assertEquals("Wrong entry returned", nodeStart, entry.getNodeState()); assertTrue("We should still have elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The second element should be n1", N1, entry.getName()); - assertEquals("Wrong entry returned", NODE_1, entry.getNodeState()); + assertEquals("The second element should be n1", n1, entry.getName()); + assertEquals("Wrong entry returned", node1, entry.getNodeState()); assertTrue("We should still have elements left to loop through", it.hasNext()); entry = it.next(); - assertEquals("The third element should be n0", N0, entry.getName()); - assertEquals("Wrong entry returned", NODE_0, entry.getNodeState()); + assertEquals("The third element should be n0", n0, entry.getName()); + assertEquals("Wrong entry returned", node0, entry.getNodeState()); assertFalse("We should be at the end of the list", it.hasNext()); } @@ -319,12 +320,12 @@ */ @Test public void childNodeEntriesNoItemsWithStart() { - NodeState NODE_START = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + NodeState nodeStart = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); // setting-up the index - index.setChildNode(START, NODE_START); + index.setChildNode(START, nodeStart); Iterable children = store.getChildNodeEntries(index.getNodeState(), true); assertEquals("Wrong size of Iterable", 1, Iterators.size(children.iterator())); @@ -333,7 +334,7 @@ assertTrue("We should have at least 1 element", it.hasNext()); ChildNodeEntry entry = it.next(); assertEquals(":start is expected", START, entry.getName()); - assertEquals("wrong node returned", NODE_START, entry.getNodeState()); + assertEquals("wrong node returned", nodeStart, entry.getNodeState()); assertFalse("We should be at the end of the list", it.hasNext()); } @@ -344,7 +345,7 @@ */ @Test public void childNodeEntriesNewIndexWithStart() { - NodeState NODE_START = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + NodeState nodeStart = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); @@ -355,7 +356,7 @@ assertTrue("at least one item expected", children.hasNext()); ChildNodeEntry child = children.next(); assertEquals(START, child.getName()); - assertEquals(NODE_START, child.getNodeState()); + assertEquals(nodeStart, child.getNodeState()); assertFalse(children.hasNext()); } @@ -570,44 +571,44 @@ @Test public void singleKeyUpdate() { - final String N0 = KEYS[0]; - final String N1 = KEYS[1]; - final String PATH = "/content/foobar"; - final String[] NODES = Iterables.toArray(PathUtils.elements(PATH), String.class); + final String n0 = KEYS[0]; + final String n1 = KEYS[1]; + final String path = "/content/foobar"; + final String[] nodes = Iterables.toArray(PathUtils.elements(path), String.class); IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); NodeBuilder node = null; // Stage 1 - store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N0)); + store.update(index, path, EMPTY_KEY_SET, newHashSet(n0)); node = index.getChildNode(START); assertTrue(":start should exists", node.exists()); - assertEquals(":start should point to n0", N0, node.getString(NEXT)); + assertEquals(":start should point to n0", n0, node.getString(NEXT)); - node = index.getChildNode(N0); + node = index.getChildNode(n0); assertTrue(":index should have n0", node.exists()); assertTrue("n0 should point nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); - node = node.getChildNode(NODES[0]); + node = node.getChildNode(nodes[0]); assertTrue("n0 should have /content", node.exists()); - node = node.getChildNode(NODES[1]); + node = node.getChildNode(nodes[1]); assertTrue("/content should contain /foobar", node.exists()); assertTrue("/foobar should have match=true", node.getBoolean("match")); // Stage 2 - store.update(index, PATH, newHashSet(N0), newHashSet(N1)); + store.update(index, path, newHashSet(n0), newHashSet(n1)); node = index.getChildNode(START); - assertEquals(":start should now point to N1", N1, node.getString(NEXT)); + assertEquals(":start should now point to n1", n1, node.getString(NEXT)); - node = index.getChildNode(N1); - assertTrue("N1 should exists", node.exists()); - assertTrue("N1 should point nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); + node = index.getChildNode(n1); + assertTrue("n1 should exists", node.exists()); + assertTrue("n1 should point nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); - node = node.getChildNode(NODES[0]); - assertTrue("N1 should have /content", node.exists()); + node = node.getChildNode(nodes[0]); + assertTrue("n1 should have /content", node.exists()); - node = node.getChildNode(NODES[1]); + node = node.getChildNode(nodes[1]); assertTrue("/content should contain /foobar", node.exists()); assertTrue("/foobar should have match=true", node.getBoolean("match")); } @@ -632,18 +633,18 @@ @Test public void findPrevious1ItemIndex() { final OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); - final String N0 = KEYS[0]; - final NodeState NODE_START = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, N0).getNodeState(); - final NodeState NODE_0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); + final String n0 = KEYS[0]; + final NodeState nodeStart = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, n0).getNodeState(); + final NodeState node0 = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState(); final NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); - index.setChildNode(START, NODE_START); - index.setChildNode(N0, NODE_0); + index.setChildNode(START, nodeStart); + index.setChildNode(n0, node0); NodeState indexState = index.getNodeState(); - ChildNodeEntry previous = store.findPrevious(indexState, NODE_0); + ChildNodeEntry previous = store.findPrevious(indexState, node0); assertNotNull(previous); - assertEquals("the :start node is expected", NODE_START, previous.getNodeState()); + assertEquals("the :start node is expected", nodeStart, previous.getNodeState()); } /** @@ -692,69 +693,68 @@ */ @Test public void documentChangingKey() { - final String PATH0 = "/content/one"; - final String PATH1 = "/content/two"; - final String N0 = KEYS[0]; - final String N1 = KEYS[1]; + final String path0 = "/content/one"; + final String path1 = "/content/two"; + final String n0 = KEYS[0]; + final String n1 = KEYS[1]; NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); // Stage 1 - initialising the index - store.update(index, PATH0, EMPTY_KEY_SET, newHashSet(N0)); - store.update(index, PATH1, EMPTY_KEY_SET, newHashSet(N0)); + store.update(index, path0, EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, path1, EMPTY_KEY_SET, newHashSet(n0)); // ensuring the right structure assertTrue(index.hasChildNode(START)); - assertTrue(index.hasChildNode(N0)); - assertFalse(index.hasChildNode(N1)); + assertTrue(index.hasChildNode(n0)); + assertFalse(index.hasChildNode(n1)); NodeBuilder node = index.getChildNode(START); - assertEquals(":start pointing to wrong node", N0, node.getString(NEXT)); + assertEquals(":start pointing to wrong node", n0, node.getString(NEXT)); - node = index.getChildNode(N0); - assertTrue("N0 should go nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); + node = index.getChildNode(n0); + assertTrue("n0 should go nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); // checking the first document - String[] path = Iterables.toArray(PathUtils.elements(PATH0), String.class); + String[] path = Iterables.toArray(PathUtils.elements(path0), String.class); node = node.getChildNode(path[0]); assertTrue(node.exists()); node = node.getChildNode(path[1]); assertTrue(node.exists()); assertTrue(node.getBoolean("match")); - path = Iterables.toArray(PathUtils.elements(PATH0), String.class); - node = index.getChildNode(N0).getChildNode(path[0]); + path = Iterables.toArray(PathUtils.elements(path0), String.class); + node = index.getChildNode(n0).getChildNode(path[0]); assertTrue(node.exists()); node = node.getChildNode(path[1]); assertTrue(node.exists()); assertTrue(node.getBoolean("match")); // Stage 2 - store.update(index, PATH1, newHashSet(N0), newHashSet(N1)); + store.update(index, path1, newHashSet(n0), newHashSet(n1)); assertTrue(index.hasChildNode(START)); - assertTrue(index.hasChildNode(N0)); - assertTrue(index.hasChildNode(N1)); + assertTrue(index.hasChildNode(n0)); + assertTrue(index.hasChildNode(n1)); node = index.getChildNode(START); - assertEquals(":start pointing to wrong node", N0, node.getString(NEXT)); + assertEquals(":start pointing to wrong node", n0, node.getString(NEXT)); - node = index.getChildNode(N0); - assertEquals(N1, node.getString(NEXT)); - path = Iterables.toArray(PathUtils.elements(PATH0), String.class); + node = index.getChildNode(n0); + assertEquals(n1, node.getString(NEXT)); + path = Iterables.toArray(PathUtils.elements(path0), String.class); node = node.getChildNode(path[0]); assertTrue(node.exists()); node = node.getChildNode(path[1]); assertTrue(node.exists()); assertTrue(node.getBoolean("match")); - path = Iterables.toArray(PathUtils.elements(PATH1), String.class); - node = index.getChildNode(N0).getChildNode(path[0]);// we know both the - // documents share - // the same /content - assertFalse("/content/two should no longer be under N0", node.hasChildNode(path[1])); + path = Iterables.toArray(PathUtils.elements(path1), String.class); + // we know both the documents share the same /content + node = index.getChildNode(n0).getChildNode(path[0]); + assertFalse("/content/two should no longer be under n0", node.hasChildNode(path[1])); - node = index.getChildNode(N1); - assertTrue("N1 should point nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); - path = Iterables.toArray(PathUtils.elements(PATH1), String.class); + node = index.getChildNode(n1); + assertTrue("n1 should point nowhere", Strings.isNullOrEmpty(node.getString(NEXT))); + path = Iterables.toArray(PathUtils.elements(path1), String.class); node = node.getChildNode(path[0]); assertTrue(node.exists()); node = node.getChildNode(path[1]); @@ -792,20 +792,20 @@ */ @Test public void deleteTheOnlyDocument() { - final String N0 = KEYS[0]; - final String PATH = "/sampledoc"; + final String n0 = KEYS[0]; + final String path = "/sampledoc"; NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); // Stage 1 - initialising the index - store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N0)); + store.update(index, path, EMPTY_KEY_SET, newHashSet(n0)); // we assume it works and therefore not checking the status of the index // let's go straight to Stage 2 // Stage 2 - store.update(index, PATH, newHashSet(N0), EMPTY_KEY_SET); - assertFalse("The node should have been removed", index.hasChildNode(N0)); + store.update(index, path, newHashSet(n0), EMPTY_KEY_SET); + assertFalse("The node should have been removed", index.hasChildNode(n0)); assertTrue("as the index should be empty, :start should point nowhere", Strings.isNullOrEmpty(index.getChildNode(START).getString(NEXT))); } @@ -846,30 +846,30 @@ */ @Test public void deleteOneOfTheDocuments() { - final String N0 = KEYS[0]; - final String DOC1 = "doc1"; - final String DOC2 = "doc2"; - final String PATH1 = "/" + DOC1; - final String PATH2 = "/" + DOC2; + final String n0 = KEYS[0]; + final String doc1 = "doc1"; + final String doc2 = "doc2"; + final String path1 = "/" + doc1; + final String path2 = "/" + doc2; NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); - store.update(index, PATH1, EMPTY_KEY_SET, newHashSet(N0)); - store.update(index, PATH2, EMPTY_KEY_SET, newHashSet(N0)); + store.update(index, path1, EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, path2, EMPTY_KEY_SET, newHashSet(n0)); // we trust the store at this point and skip a double-check. Let's move // to Stage 2! - store.update(index, PATH1, newHashSet(N0), EMPTY_KEY_SET); + store.update(index, path1, newHashSet(n0), EMPTY_KEY_SET); assertTrue(index.hasChildNode(START)); - assertTrue(index.hasChildNode(N0)); - assertEquals(":start should still point to N0", N0, index.getChildNode(START).getString(NEXT)); - assertTrue("n0 should point nowhere", Strings.isNullOrEmpty(index.getChildNode(N0).getString(NEXT))); + assertTrue(index.hasChildNode(n0)); + assertEquals(":start should still point to n0", n0, index.getChildNode(START).getString(NEXT)); + assertTrue("n0 should point nowhere", Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); - assertFalse(index.getChildNode(N0).hasChildNode(DOC1)); - assertTrue(index.getChildNode(N0).hasChildNode(DOC2)); - assertTrue(index.getChildNode(N0).getChildNode(DOC2).getBoolean("match")); + assertFalse(index.getChildNode(n0).hasChildNode(doc1)); + assertTrue(index.getChildNode(n0).hasChildNode(doc2)); + assertTrue(index.getChildNode(n0).getChildNode(doc2).getBoolean("match")); } /** @@ -930,46 +930,318 @@ */ @Test public void deleteTheOnlyDocumentInMultiKeysIndex() { - final String PATH0 = "/content/doc0"; - final String PATH1 = "/content/doc1"; - final String PATH2 = "/content/doc2"; - final String N0 = KEYS[2]; - final String N1 = KEYS[0]; - final String N2 = KEYS[1]; + final String path0 = "/content/doc0"; + final String path1 = "/content/doc1"; + final String path2 = "/content/doc2"; + final String n0 = KEYS[2]; + final String n1 = KEYS[0]; + final String n2 = KEYS[1]; NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(); // Stage 1 - store.update(index, PATH0, EMPTY_KEY_SET, newHashSet(N0)); - store.update(index, PATH1, EMPTY_KEY_SET, newHashSet(N1)); - store.update(index, PATH2, EMPTY_KEY_SET, newHashSet(N2)); + store.update(index, path0, EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, path1, EMPTY_KEY_SET, newHashSet(n1)); + store.update(index, path2, EMPTY_KEY_SET, newHashSet(n2)); // as we trust the store we skip the check and goes straight to Stage 2. // Stage 2 - store.update(index, PATH2, newHashSet(N2), EMPTY_KEY_SET); + store.update(index, path2, newHashSet(n2), EMPTY_KEY_SET); // checking key nodes assertTrue(index.hasChildNode(START)); - assertTrue(index.hasChildNode(N0)); - assertTrue(index.hasChildNode(N1)); - assertFalse(index.hasChildNode(N2)); + assertTrue(index.hasChildNode(n0)); + assertTrue(index.hasChildNode(n1)); + assertFalse(index.hasChildNode(n2)); // checking pointers - assertEquals(N1, index.getChildNode(START).getString(NEXT)); - assertEquals(N0, index.getChildNode(N1).getString(NEXT)); - assertTrue(Strings.isNullOrEmpty(index.getChildNode(N0).getString(NEXT))); + assertEquals(n1, index.getChildNode(START).getString(NEXT)); + assertEquals(n0, index.getChildNode(n1).getString(NEXT)); + assertTrue(Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); // checking sub-nodes - String[] subNodes = Iterables.toArray(PathUtils.elements(PATH0), String.class); - assertTrue(index.getChildNode(N0).hasChildNode(subNodes[0])); - assertTrue(index.getChildNode(N0).getChildNode(subNodes[0]).hasChildNode(subNodes[1])); - assertTrue(index.getChildNode(N0).getChildNode(subNodes[0]).getChildNode(subNodes[1]).getBoolean("match")); + String[] subNodes = Iterables.toArray(PathUtils.elements(path0), String.class); + assertTrue(index.getChildNode(n0).hasChildNode(subNodes[0])); + assertTrue(index.getChildNode(n0).getChildNode(subNodes[0]).hasChildNode(subNodes[1])); + assertTrue(index.getChildNode(n0).getChildNode(subNodes[0]).getChildNode(subNodes[1]).getBoolean("match")); + + subNodes = Iterables.toArray(PathUtils.elements(path1), String.class); + assertTrue(index.getChildNode(n1).hasChildNode(subNodes[0])); + assertTrue(index.getChildNode(n1).getChildNode(subNodes[0]).hasChildNode(subNodes[1])); + assertTrue(index.getChildNode(n1).getChildNode(subNodes[0]).getChildNode(subNodes[1]).getBoolean("match")); + } + + /** + * test the insert of 1 item in a descending order index. it should not really matter but just + * checking we don't break anything + * + * expecting + * + * + * :index : { + * :start : { :next = n0 }, + * n0 : { :next = } + * } + * + */ + @Test + public void descendingOrderInsert1item() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[1]; + + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + assertEquals(":start should point to the first node", n0, index.getChildNode(START) + .getString(NEXT)); + assertTrue("the first node should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + } + + /** + *

test the insertion of 2 already ordered items

+ * + *

expected

+ * + * + * :index : { + * :start : { :next=n0 }, + * n0 : { :next=n1 }, + * n1 : { :next=} + * } + * + */ + @Test + public void descendingOrderInsert2itemsAlreadyOrdered() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[1]; + String n1 = KEYS[0]; + + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); + assertEquals(":start should point to the first node", n0, index.getChildNode(START) + .getString(NEXT)); + assertEquals("n0 should point to n1", n1, index.getChildNode(n0).getString(NEXT)); + assertTrue("n1 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n1).getString(NEXT))); + } + + /** + * test the insert of 4 shuffled items in a descending ordered index + * + * expected: + * + * + * :index : { + * :start : { :next= n1}, + * n0 : { :next= n3}, + * n1 : { :next= n2}, + * n2: { :next= n0}, + * n3 : { :next= }, + * } + * + */ + @Test + public void descendingOrderInsert4ShuffledItems() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[1]; + String n1 = KEYS[3]; + String n2 = KEYS[2]; + String n3 = KEYS[0]; + + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n2)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n3)); + assertEquals(":start should point to n1", n1, index.getChildNode(START).getString(NEXT)); + assertEquals("n0 should point to n3", n3, index.getChildNode(n0).getString(NEXT)); + assertEquals("n1 should point to n2", n2, index.getChildNode(n1).getString(NEXT)); + assertEquals("n2 should point to n1", n0, index.getChildNode(n2).getString(NEXT)); + assertTrue("n3 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n3).getString(NEXT))); + } + + /** + * Tests the insert of 4 items that will always have to be added at the beginning of the list. + * Just to simulate the use-case of lastModified DESC. + * + * + * Stage 1 + * ======= + * + * :index : { + * :start : { :next=n0 }, + * n0 : { :next= } + * } + * + * Stage 2 + * ======= + * + * :index : { + * :start : { :next=n1 }, + * n0 : { :next= }, + * n1 : { :next=n0 } + * } + * + * Stage 3 + * ======= + * + * :index : { + * :start : { :next=n2 }, + * n0 : { :next= }, + * n1 : { :next=n0 }, + * n2 : { :next=n1 } + * } + * + * Stage 4 + * ======= + * + * :index : { + * :start : { :next=n3 }, + * n0 : { :next= }, + * n1 : { :next=n0 }, + * n2 : { :next=n1 }, + * n3 : { :next=n2 } + * } + * + */ + @Test + public void descendingOrder4StagedInsertsAlwaysGreater() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[0]; + String n1 = KEYS[1]; + String n2 = KEYS[2]; + String n3 = KEYS[3]; + + // Stage 1 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + assertEquals(":start should point to n0", n0, index.getChildNode(START).getString(NEXT)); + assertTrue("n0 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + + // Stage 2 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); + assertEquals(":start should point to n1", n1, index.getChildNode(START).getString(NEXT)); + assertEquals("n1 should point to n0", n0, index.getChildNode(n1).getString(NEXT)); + assertTrue("n0 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + + // Stage 3 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n2)); + assertEquals(":start should point to n2", n2, index.getChildNode(START).getString(NEXT)); + assertEquals("n2 should point to n1", n1, index.getChildNode(n2).getString(NEXT)); + assertEquals("n1 should point to n0", n0, index.getChildNode(n1).getString(NEXT)); + assertTrue("n0 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + + // Stage 4 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n3)); + assertEquals(":start should point to n3", n3, index.getChildNode(START).getString(NEXT)); + assertEquals("n3 should point to n2", n2, index.getChildNode(n3).getString(NEXT)); + assertEquals("n2 should point to n1", n1, index.getChildNode(n2).getString(NEXT)); + assertEquals("n1 should point to n0", n0, index.getChildNode(n1).getString(NEXT)); + assertTrue("n0 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + } + + /** + * test finding a previous item in a descending ordered index. + * + * + * Stage 1 + * ======= + * + * :index { + * :start : { :next=n0 }, + * n0 : { :next= } + * } + * + * findPrevious(n0)=:start + * + * Stage 2 + * ======= + * + * :index { + * :start : { :next=n1 }, + * n0 : { :next= } + * n1 : { :next=n0 } + * } + * + * findPrevious(n0)=n1; + * + */ + @Test + public void descendingOrderFindPrevious() { + OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[0]; + String n1 = KEYS[1]; + NodeState indexState; + NodeState previous; + NodeState node; + + //Stage 1 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + indexState = index.getNodeState(); + node = indexState.getChildNode(n0); + previous = indexState.getChildNode(START); + assertEquals(previous, store.findPrevious(indexState, node).getNodeState()); + + //Stage 2 + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); + indexState = index.getNodeState(); + node = indexState.getChildNode(n0); + previous = indexState.getChildNode(n1); + assertEquals(previous, store.findPrevious(indexState, node).getNodeState()); + } + + /** + * test the iteration of the descending index with 2 shuffled items + * + * + * :index : { + * :start : { :next=n1 }, + * n0 : { :next= }, + * n1 : { :next=n0 } + * } + * + */ + @Test + public void descendingOrderChildNodeEntriesACoupleOfMixedItems() { + OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy( + OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + final String n0 = KEYS[0]; + final String n1 = KEYS[1]; + + // setting-up the index structure + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); - subNodes = Iterables.toArray(PathUtils.elements(PATH1), String.class); - assertTrue(index.getChildNode(N1).hasChildNode(subNodes[0])); - assertTrue(index.getChildNode(N1).getChildNode(subNodes[0]).hasChildNode(subNodes[1])); - assertTrue(index.getChildNode(N1).getChildNode(subNodes[0]).getChildNode(subNodes[1]).getBoolean("match")); + NodeState indexState = index.getNodeState(); + NodeState node0 = indexState.getChildNode(n0); + NodeState node1 = indexState.getChildNode(n1); + + Iterable children = store.getChildNodeEntries(indexState); + assertNotNull("The iterable cannot be null", children); + assertEquals("Expecting 2 items in the index", 2, Iterators.size(children.iterator())); + + // ensuring the right sequence + ChildNodeEntry entry = null; + children = store.getChildNodeEntries(indexState); + Iterator it = children.iterator(); + assertTrue("We should have 2 elements left to loop through", it.hasNext()); + entry = it.next(); + assertEquals("The first element should be n1", n1, entry.getName()); + assertEquals("Wrong entry returned", node1, entry.getNodeState()); + assertTrue("We should have 1 elements left to loop through", it.hasNext()); + entry = it.next(); + assertEquals("The second element should be n0", n0, entry.getName()); + assertEquals("Wrong entry returned", node0, entry.getNodeState()); + assertFalse("We should have be at the end of the list", it.hasNext()); } }