diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java index cb8aa90..a35cb3a 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java @@ -30,6 +30,7 @@ import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPER 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 @@ public class IndexUtils { @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 @@ public class IndexUtils { && 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)); + } + } + } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java index e45a1db..85b9737 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java @@ -17,10 +17,64 @@ 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 + */ + public static @Nullable @CheckForNull 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; } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java index 0fb6c35..908bcf2 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java @@ -19,10 +19,13 @@ package org.apache.jackrabbit.oak.plugins.index.property; 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; public class OrderedPropertyIndex extends PropertyIndex { - + private static final Logger log = LoggerFactory.getLogger(OrderedPropertyIndex.class); @Override public String getIndexName() { return TYPE; @@ -32,4 +35,11 @@ public class OrderedPropertyIndex extends PropertyIndex { 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; + } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java index 36ff888..601c1fc 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java @@ -22,14 +22,13 @@ import java.util.Set; 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; @@ -39,18 +38,29 @@ import com.google.common.base.Strings; 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 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); @@ -64,13 +74,27 @@ public class OrderedPropertyIndexEditor extends PropertyIndexEditor { 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 +104,11 @@ public class OrderedPropertyIndexEditor extends PropertyIndexEditor { */ @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 +126,11 @@ public class OrderedPropertyIndexEditor extends PropertyIndexEditor { 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; } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java index b0dbde3..9e60ba3 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java @@ -26,6 +26,8 @@ import java.util.Set; 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; @@ -70,6 +72,20 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg .setProperty(NEXT, "") .getNodeState(); + /** + * 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); @@ -86,12 +102,8 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg } 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()); + Iterable children = getChildNodeEntries(index.getNodeState(), + true); for (ChildNodeEntry child : children) { nextKey = child.getNodeState().getString(NEXT); if (Strings.isNullOrEmpty(nextKey)) { @@ -100,7 +112,7 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg localkey = index.child(key); localkey.setProperty(NEXT, ""); } else { - if (key.compareTo(nextKey) < 0) { + if (isInsertHere(key, nextKey)) { index.getChildNode(child.getName()).setProperty(NEXT, key); localkey = index.child(key); localkey.setProperty(NEXT, nextKey); @@ -109,11 +121,25 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg } } } - } 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) { @@ -138,6 +164,20 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg } } + /** + * 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; diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java new file mode 100644 index 0000000..ea51961 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java @@ -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++; + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java new file mode 100644 index 0000000..69b4c13 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/LowCostOrderedPropertyIndexProvider.java @@ -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; + } + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java new file mode 100644 index 0000000..9180e8f --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java @@ -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); + } +} diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java index f75fbdb..9c6c028 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java @@ -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.easymock.EasyMock.replay; 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,6 +30,8 @@ import java.util.Arrays; 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; @@ -81,4 +83,64 @@ public class OrderedPropertyIndexEditorTest { 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)); + } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java index 5329c4c..205f4a7 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java @@ -20,138 +20,29 @@ import static junit.framework.Assert.assertEquals; 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 @@ public class OrderedPropertyIndexQueryTest extends AbstractQueryTest { } @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 @@ public class OrderedPropertyIndexQueryTest extends AbstractQueryTest { } /** - * 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 @@ public class OrderedPropertyIndexQueryTest extends AbstractQueryTest { 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 @@ public class OrderedPropertyIndexQueryTest extends AbstractQueryTest { 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. 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); diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java new file mode 100644 index 0000000..ce96c17 --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java @@ -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 diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java index fb4a51d..49e3ed9 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java @@ -29,6 +29,7 @@ import java.util.Iterator; 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; @@ -972,4 +973,276 @@ public class OrderedContentMirrorStorageStrategyTest { 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)); + + 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()); + } }