From 418e71c23dc36abae67e9039ba574ac0da776946 Mon Sep 17 00:00:00 2001
From: Davide Giannella
- * To define a property index on a subtree you have to add an oak:index node.
- *
+ * To define a property index on a subtree you have to add an oak:index node.
* Next (as a child node) follows the index definition node that:
*
*
oak:QueryIndexDefinition
* Optionally you can specify - *
unique flag to truedeclaringNodeTypes propertyunique flag to truedeclaringNodeTypes
+ * property* Notes: *
propertyNames can be a list of properties, and it is optional.in case it is missing, the node name will be used as a property name reference valuepropertyNames can be a list of properties, and it is optional.in case it is missing, the node name
+ * will be used as a property name reference valuereindex is a property that when set to true, triggers a full content reindex.
*
@@ -64,19 +61,17 @@ public class PropertyIndexLookup {
* The cost overhead to use the index in number of read operations.
*/
private static final int COST_OVERHEAD = 2;
-
+
/**
* The maximum cost when the index can be used.
*/
private static final int MAX_COST = 100;
/** Index storage strategy */
- private static final IndexStoreStrategy MIRROR =
- new ContentMirrorStoreStrategy();
+ private static final IndexStoreStrategy MIRROR = new ContentMirrorStoreStrategy();
/** Index storage strategy */
- private static final IndexStoreStrategy UNIQUE =
- new UniqueEntryStoreStrategy();
+ private static final IndexStoreStrategy UNIQUE = new UniqueEntryStoreStrategy();
private final NodeState root;
@@ -85,13 +80,15 @@ public class PropertyIndexLookup {
}
/**
- * Checks whether the named property is indexed somewhere along the given
- * path. Lookup starts at the current path (at the root of this object) and
- * traverses down the path.
+ * Checks whether the named property is indexed somewhere along the given path. Lookup starts at the current path
+ * (at the root of this object) and traverses down the path.
*
- * @param propertyName property name
- * @param path lookup path
- * @param filter for the node type restriction (null if no node type restriction)
+ * @param propertyName
+ * property name
+ * @param path
+ * lookup path
+ * @param filter
+ * for the node type restriction (null if no node type restriction)
* @return true if the property is indexed
*/
public boolean isIndexed(String propertyName, String path, Filter filter) {
@@ -100,12 +97,11 @@ public class PropertyIndexLookup {
}
NodeState node = root;
- Iterator it = PathUtils.elements(path).iterator();
- while (it.hasNext()) {
+ for(String s : PathUtils.elements(path)){
if (getIndexNode(node, propertyName, filter) != null) {
return true;
}
- node = node.getChildNode(it.next());
+ node = node.getChildNode(s);
}
return false;
}
@@ -117,8 +113,8 @@ public class PropertyIndexLookup {
}
return getStrategy(indexMeta).query(filter, propertyName, indexMeta, encode(value));
}
-
- private static IndexStoreStrategy getStrategy(NodeState indexMeta) {
+
+ IndexStoreStrategy getStrategy(NodeState indexMeta) {
if (indexMeta.getBoolean(IndexConstants.UNIQUE_PROPERTY_NAME)) {
return UNIQUE;
}
@@ -130,24 +126,21 @@ public class PropertyIndexLookup {
if (indexMeta == null) {
return Double.POSITIVE_INFINITY;
}
- return COST_OVERHEAD +
- getStrategy(indexMeta).count(indexMeta, encode(value), MAX_COST);
+ return COST_OVERHEAD + getStrategy(indexMeta).count(indexMeta, encode(value), MAX_COST);
}
/**
- * Get the node with the index definition for the given property, if there
- * is an applicable index with data.
+ * Get the node with the index definition for the given property, if there is an applicable index with data.
*
- * @param propertyName the property name
- * @param filter the filter (which contains information of all supertypes,
- * unless the filter matches all types)
- * @return the node where the index definition (metadata) is stored (the
- * parent of ":index"), or null if no index definition or index data
- * node was found
+ * @param propertyName
+ * the property name
+ * @param filter
+ * the filter (which contains information of all supertypes, unless the filter matches all types)
+ * @return the node where the index definition (metadata) is stored (the parent of ":index"), or null if no index
+ * definition or index data node was found
*/
@Nullable
- private static NodeState getIndexNode(
- NodeState node, String propertyName, Filter filter) {
+ private NodeState getIndexNode(NodeState node, String propertyName, Filter filter) {
// keep a fallback to a matching index def that has *no* node type constraints
// (initially, there is no fallback)
NodeState fallback = null;
@@ -156,7 +149,7 @@ public class PropertyIndexLookup {
for (ChildNodeEntry entry : state.getChildNodeEntries()) {
NodeState index = entry.getNodeState();
PropertyState type = index.getProperty(TYPE_PROPERTY_NAME);
- if (type == null || type.isArray() || !TYPE.equals(type.getValue(Type.STRING))) {
+ if (type == null || type.isArray() || !getType().equals(type.getValue(Type.STRING))) {
continue;
}
if (contains(index.getNames(PROPERTY_NAMES), propertyName)) {
@@ -184,7 +177,16 @@ public class PropertyIndexLookup {
}
return fallback;
}
-
+
+ /**
+ * retrieve the type of the index
+ *
+ * @return
+ */
+ String getType() {
+ return TYPE;
+ }
+
private static Set getSuperTypes(Filter filter) {
if (filter != null && !filter.matchesAllTypes()) {
return filter.getSupertypes();
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
index cac52cf..95b640c 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
@@ -24,6 +24,8 @@ import java.util.Deque;
import java.util.Iterator;
import java.util.Set;
+import javax.annotation.Nonnull;
+
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
@@ -65,344 +67,370 @@ import com.google.common.collect.Sets;
*/
public class ContentMirrorStoreStrategy implements IndexStoreStrategy {
- static final Logger LOG = LoggerFactory.getLogger(ContentMirrorStoreStrategy.class);
-
- @Override
- public void update(
- NodeBuilder index, String path,
- Set beforeKeys, Set afterKeys) {
- for (String key : beforeKeys) {
- remove(index, key, path);
- }
- for (String key : afterKeys) {
- insert(index, key, path);
- }
- }
-
- private static void remove(NodeBuilder index, String key, String value) {
- NodeBuilder builder = index.getChildNode(key);
- if (builder.exists()) {
- // Collect all builders along the given path
- Deque builders = newArrayDeque();
+ static final Logger LOG = LoggerFactory.getLogger(ContentMirrorStoreStrategy.class);
+
+ @Override
+ public void update(
+ NodeBuilder index, String path,
+ Set beforeKeys, Set afterKeys) {
+ for (String key : beforeKeys) {
+ remove(index, key, path);
+ }
+ for (String key : afterKeys) {
+ insert(index, key, path);
+ }
+ }
+
+ private void remove(NodeBuilder index, String key, String value) {
+ NodeBuilder builder = index.getChildNode(key);
+ if (builder.exists()) {
+ // Collect all builders along the given path
+ Deque builders = newArrayDeque();
+ builders.addFirst(builder);
+
+ // Descend to the correct location in the index tree
+ for (String name : PathUtils.elements(value)) {
+ builder = builder.getChildNode(name);
builders.addFirst(builder);
+ }
- // Descend to the correct location in the index tree
- for (String name : PathUtils.elements(value)) {
- builder = builder.getChildNode(name);
- builders.addFirst(builder);
- }
+ // Drop the match value, if present
+ if (builder.exists()) {
+ builder.removeProperty("match");
+ }
- // Drop the match value, if present
- if (builder.exists()) {
- builder.removeProperty("match");
- }
+ // Prune all index nodes that are no longer needed
+ prune(index, builders);
+ }
+ }
- // Prune all index nodes that are no longer needed
- for (NodeBuilder node : builders) {
- if (node.getBoolean("match") || node.getChildNodeCount(1) > 0) {
- return;
- } else if (node.exists()) {
- node.remove();
- }
- }
- }
- }
-
- private static void insert(NodeBuilder index, String key, String value) {
- NodeBuilder builder = index.child(key);
- for (String name : PathUtils.elements(value)) {
- builder = builder.child(name);
- }
- builder.setProperty("match", true);
- }
-
- public Iterable query(final Filter filter, final String indexName,
- final NodeState indexMeta, final String indexStorageNodeName,
- final Iterable values) {
- final NodeState index = indexMeta.getChildNode(indexStorageNodeName);
- return new Iterable() {
- @Override
- public Iterator iterator() {
- PathIterator it = new PathIterator(filter, indexName);
- if (values == null) {
- it.setPathContainsValue(true);
- it.enqueue(index.getChildNodeEntries().iterator());
- } else {
- for (String p : values) {
- NodeState property = index.getChildNode(p);
- if (property.exists()) {
- // we have an entry for this value, so use it
- it.enqueue(Iterators.singletonIterator(
- new MemoryChildNodeEntry("", property)));
- }
- }
- }
- return it;
- }
- };
- }
-
- @Override
- public Iterable query(final Filter filter, final String indexName,
- final NodeState indexMeta, final Iterable values) {
- return query(filter, indexName, indexMeta, INDEX_CONTENT_NODE_NAME, values);
- }
-
- @Override
- public long count(NodeState indexMeta, Set values, int max) {
- return count(indexMeta, INDEX_CONTENT_NODE_NAME, values, max);
- }
-
- public long count(NodeState indexMeta, final String indexStorageNodeName,
- Set values, int max) {
- NodeState index = indexMeta.getChildNode(indexStorageNodeName);
- int count = 0;
- if (values == null) {
- PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME);
- if (ec != null) {
- return ec.getValue(Type.LONG);
- }
- CountingNodeVisitor v = new CountingNodeVisitor(max);
- v.visit(index);
- count = v.getEstimatedCount();
- // "is not null" queries typically read more data
- count *= 10;
- } else {
- int size = values.size();
- if (size == 0) {
- return 0;
- }
- max = Math.max(10, max / size);
- int i = 0;
- for (String p : values) {
- if (count > max && i > 3) {
- // the total count is extrapolated from the the number
- // of values counted so far to the total number of values
- count = count * size / i;
- break;
- }
- NodeState s = index.getChildNode(p);
- if (s.exists()) {
- CountingNodeVisitor v = new CountingNodeVisitor(max);
- v.visit(s);
- count += v.getEstimatedCount();
- }
- i++;
- }
- }
- return count;
- }
-
- /**
- * An iterator over paths within an index node.
- */
- static class PathIterator implements Iterator {
-
- private final Filter filter;
- private final String indexName;
- private final Deque> nodeIterators =
- Queues.newArrayDeque();
- private int readCount;
- private boolean init;
- private boolean closed;
- private String parentPath;
- private String currentPath;
- private boolean pathContainsValue;
-
- /**
- * Keep the returned path, to avoid returning duplicate entries.
- */
- private final Set knownPaths = Sets.newHashSet();
-
- PathIterator(Filter filter, String indexName) {
- this.filter = filter;
- this.indexName = indexName;
- parentPath = "";
- currentPath = "/";
- }
-
- void enqueue(Iterator extends ChildNodeEntry> it) {
- nodeIterators.addLast(it);
- }
-
- void setPathContainsValue(boolean pathContainsValue) {
- if (init) {
- throw new IllegalStateException("This iterator is already initialized");
- }
- this.pathContainsValue = pathContainsValue;
- }
-
- @Override
- public boolean hasNext() {
- if (!closed && !init) {
- fetchNext();
- init = true;
+ /**
+ * Physically prune a list of nodes from the index
+ *
+ * @param index the current index
+ * @param builders list of nodes to prune
+ */
+ void prune(final NodeBuilder index, final Deque builders){
+ for (NodeBuilder node : builders) {
+ if (node.getBoolean("match") || node.getChildNodeCount(1) > 0) {
+ return;
+ } else if (node.exists()) {
+ node.remove();
+ }
+ }
+ }
+
+ private void insert(NodeBuilder index, String key, String value) {
+ // NodeBuilder builder = index.child(key);
+ NodeBuilder builder = fetchKeyNode(index, key);
+ for (String name : PathUtils.elements(value)) {
+ builder = builder.child(name);
+ }
+ builder.setProperty("match", true);
+ }
+
+ /**
+ * fetch from the index the key node
+ *
+ * @param index the current index root
+ * @param key the 'key' to fetch from the repo
+ * @return the node representing the key
+ */
+ NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index,@Nonnull String key){
+ return index.child(key);
+ }
+
+ public Iterable query(final Filter filter, final String indexName,
+ final NodeState indexMeta, final String indexStorageNodeName,
+ final Iterable values) {
+ final NodeState index = indexMeta.getChildNode(indexStorageNodeName);
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ PathIterator it = new PathIterator(filter, indexName);
+ if (values == null) {
+ it.setPathContainsValue(true);
+ it.enqueue(getChildNodeEntries(index).iterator());
+ } else {
+ for (String p : values) {
+ NodeState property = index.getChildNode(p);
+ if (property.exists()) {
+ // we have an entry for this value, so use it
+ it.enqueue(Iterators.singletonIterator(
+ new MemoryChildNodeEntry("", property)));
+ }
+ }
}
- return !closed;
- }
-
- private void fetchNext() {
- while (true) {
- fetchNextPossiblyDuplicate();
- if (closed) {
- return;
- }
- if (pathContainsValue) {
- String value = PathUtils.elements(currentPath).iterator().next();
- currentPath = PathUtils.relativize(value, currentPath);
- // don't return duplicate paths:
- // Set.add returns true if the entry was new,
- // so if it returns false, it was already known
- if (!knownPaths.add(currentPath)) {
- continue;
- }
- }
- break;
+ return it;
+ }
+ };
+ }
+
+ @Nonnull Iterable extends ChildNodeEntry> getChildNodeEntries(@Nonnull final NodeState index){
+ return index.getChildNodeEntries();
+ }
+
+ @Override
+ public Iterable query(final Filter filter, final String indexName,
+ final NodeState indexMeta, final Iterable values) {
+ return query(filter, indexName, indexMeta, INDEX_CONTENT_NODE_NAME, values);
+ }
+
+ @Override
+ public long count(NodeState indexMeta, Set values, int max) {
+ return count(indexMeta, INDEX_CONTENT_NODE_NAME, values, max);
+ }
+
+ public long count(NodeState indexMeta, final String indexStorageNodeName,
+ Set values, int max) {
+ NodeState index = indexMeta.getChildNode(indexStorageNodeName);
+ int count = 0;
+ if (values == null) {
+ PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME);
+ if (ec != null) {
+ return ec.getValue(Type.LONG);
+ }
+ CountingNodeVisitor v = new CountingNodeVisitor(max);
+ v.visit(index);
+ count = v.getEstimatedCount();
+ // "is not null" queries typically read more data
+ count *= 10;
+ } else {
+ int size = values.size();
+ if (size == 0) {
+ return 0;
+ }
+ max = Math.max(10, max / size);
+ int i = 0;
+ for (String p : values) {
+ if (count > max && i > 3) {
+ // the total count is extrapolated from the the number
+ // of values counted so far to the total number of values
+ count = count * size / i;
+ break;
}
- }
-
- private void fetchNextPossiblyDuplicate() {
- while (!nodeIterators.isEmpty()) {
- Iterator extends ChildNodeEntry> iterator = nodeIterators.getLast();
- if (iterator.hasNext()) {
- ChildNodeEntry entry = iterator.next();
-
- readCount++;
- if (readCount % 1000 == 0) {
- FilterIterators.checkReadLimit(readCount);
- LOG.warn("Traversed " + readCount + " nodes using index " + indexName + " with filter " + filter);
- }
-
- NodeState node = entry.getNodeState();
-
- String name = entry.getName();
- if (NodeStateUtils.isHidden(name)) {
- continue;
- }
- currentPath = PathUtils.concat(parentPath, name);
-
- nodeIterators.addLast(node.getChildNodeEntries().iterator());
- parentPath = currentPath;
-
- if (node.getBoolean("match")) {
- return;
- }
-
- } else {
- nodeIterators.removeLast();
- parentPath = PathUtils.getParentPath(parentPath);
- }
+ NodeState s = index.getChildNode(p);
+ if (s.exists()) {
+ CountingNodeVisitor v = new CountingNodeVisitor(max);
+ v.visit(s);
+ count += v.getEstimatedCount();
}
- currentPath = null;
- closed = true;
- }
+ i++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * An iterator over paths within an index node.
+ */
+ static class PathIterator implements Iterator {
- @Override
- public String next() {
+ private final Filter filter;
+ private final String indexName;
+ private final Deque> nodeIterators =
+ Queues.newArrayDeque();
+ private int readCount;
+ private boolean init;
+ private boolean closed;
+ private String parentPath;
+ private String currentPath;
+ private boolean pathContainsValue;
+
+ /**
+ * Keep the returned path, to avoid returning duplicate entries.
+ */
+ private final Set knownPaths = Sets.newHashSet();
+
+ PathIterator(Filter filter, String indexName) {
+ this.filter = filter;
+ this.indexName = indexName;
+ parentPath = "";
+ currentPath = "/";
+ }
+
+ void enqueue(Iterator extends ChildNodeEntry> it) {
+ nodeIterators.addLast(it);
+ }
+
+ void setPathContainsValue(boolean pathContainsValue) {
+ if (init) {
+ throw new IllegalStateException("This iterator is already initialized");
+ }
+ this.pathContainsValue = pathContainsValue;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!closed && !init) {
+ fetchNext();
+ init = true;
+ }
+ return !closed;
+ }
+
+ private void fetchNext() {
+ while (true) {
+ fetchNextPossiblyDuplicate();
if (closed) {
- throw new IllegalStateException("This iterator is closed");
+ return;
}
- if (!init) {
- fetchNext();
- init = true;
+ if (pathContainsValue) {
+ String value = PathUtils.elements(currentPath).iterator().next();
+ currentPath = PathUtils.relativize(value, currentPath);
+ // don't return duplicate paths:
+ // Set.add returns true if the entry was new,
+ // so if it returns false, it was already known
+ if (!knownPaths.add(currentPath)) {
+ continue;
+ }
}
- String result = currentPath;
- fetchNext();
- return result;
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
-
- }
-
- /**
- * A node visitor to recursively traverse a number of nodes.
- */
- interface NodeVisitor {
- void visit(NodeState state);
- }
-
- /**
- * A node visitor that counts the number of matching nodes up to a given
- * maximum, in order to estimate the number of matches.
- */
- static class CountingNodeVisitor implements NodeVisitor {
-
- /**
- * The maximum number of matching nodes to count.
- */
- final int maxCount;
-
- /**
- * The current count of matching nodes.
- */
- int count;
-
- /**
- * The current depth (number of parent nodes).
- */
- int depth;
-
- /**
- * The sum of the depth of all matching nodes. This value is used to
- * calculate the average depth.
- */
- long depthTotal;
-
- CountingNodeVisitor(int maxCount) {
- this.maxCount = maxCount;
- }
-
- @Override
- public void visit(NodeState state) {
- if (state.hasProperty("match")) {
- count++;
- depthTotal += depth;
+ break;
+ }
+ }
+
+ private void fetchNextPossiblyDuplicate() {
+ while (!nodeIterators.isEmpty()) {
+ Iterator extends ChildNodeEntry> iterator = nodeIterators.getLast();
+ if (iterator.hasNext()) {
+ ChildNodeEntry entry = iterator.next();
+
+ readCount++;
+ if (readCount % 1000 == 0) {
+ FilterIterators.checkReadLimit(readCount);
+ LOG.warn("Traversed " + readCount + " nodes using index " + indexName + " with filter " + filter);
+ }
+
+ NodeState node = entry.getNodeState();
+
+ String name = entry.getName();
+ if (NodeStateUtils.isHidden(name)) {
+ continue;
+ }
+ currentPath = PathUtils.concat(parentPath, name);
+
+ nodeIterators.addLast(node.getChildNodeEntries().iterator());
+ parentPath = currentPath;
+
+ if (node.getBoolean("match")) {
+ return;
+ }
+
+ } else {
+ nodeIterators.removeLast();
+ parentPath = PathUtils.getParentPath(parentPath);
}
- if (count < maxCount) {
- depth++;
- for (ChildNodeEntry entry : state.getChildNodeEntries()) {
- if (count >= maxCount) {
- break;
- }
- visit(entry.getNodeState());
- }
- depth--;
+ }
+ currentPath = null;
+ closed = true;
+ }
+
+ @Override
+ public String next() {
+ if (closed) {
+ throw new IllegalStateException("This iterator is closed");
+ }
+ if (!init) {
+ fetchNext();
+ init = true;
+ }
+ String result = currentPath;
+ fetchNext();
+ return result;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * A node visitor to recursively traverse a number of nodes.
+ */
+ interface NodeVisitor {
+ void visit(NodeState state);
+ }
+
+ /**
+ * A node visitor that counts the number of matching nodes up to a given
+ * maximum, in order to estimate the number of matches.
+ */
+ static class CountingNodeVisitor implements NodeVisitor {
+
+ /**
+ * The maximum number of matching nodes to count.
+ */
+ final int maxCount;
+
+ /**
+ * The current count of matching nodes.
+ */
+ int count;
+
+ /**
+ * The current depth (number of parent nodes).
+ */
+ int depth;
+
+ /**
+ * The sum of the depth of all matching nodes. This value is used to
+ * calculate the average depth.
+ */
+ long depthTotal;
+
+ CountingNodeVisitor(int maxCount) {
+ this.maxCount = maxCount;
+ }
+
+ @Override
+ public void visit(NodeState state) {
+ if (state.hasProperty("match")) {
+ count++;
+ depthTotal += depth;
+ }
+ if (count < maxCount) {
+ depth++;
+ for (ChildNodeEntry entry : state.getChildNodeEntries()) {
+ if (count >= maxCount) {
+ break;
+ }
+ visit(entry.getNodeState());
}
- }
-
- /**
- * The number of matches (at most the maximum count).
- *
- * @return the match count
- */
- int getCount() {
+ depth--;
+ }
+ }
+
+ /**
+ * The number of matches (at most the maximum count).
+ *
+ * @return the match count
+ */
+ int getCount() {
+ return count;
+ }
+
+ /**
+ * The number of estimated matches. This value might be higher than the
+ * number of counted matches, if the maximum number of matches has been
+ * reached. It is based on the average depth of matches, and the average
+ * number of child nodes.
+ *
+ * @return the estimated matches
+ */
+ int getEstimatedCount() {
+ if (count < maxCount) {
return count;
- }
-
- /**
- * The number of estimated matches. This value might be higher than the
- * number of counted matches, if the maximum number of matches has been
- * reached. It is based on the average depth of matches, and the average
- * number of child nodes.
- *
- * @return the estimated matches
- */
- int getEstimatedCount() {
- if (count < maxCount) {
- return count;
- }
- double averageDepth = (int) (depthTotal / count);
- // the number of estimated matches is higher
- // the higher the average depth of the first hits
- long estimatedNodes = (long) (count * Math.pow(1.1, averageDepth));
- estimatedNodes = Math.min(estimatedNodes, Integer.MAX_VALUE);
- return Math.max(count, (int) estimatedNodes);
- }
-
- }
+ }
+ double averageDepth = (int) (depthTotal / count);
+ // the number of estimated matches is higher
+ // the higher the average depth of the first hits
+ long estimatedNodes = (long) (count * Math.pow(1.1, averageDepth));
+ estimatedNodes = Math.min(estimatedNodes, Integer.MAX_VALUE);
+ return Math.max(count, (int) estimatedNodes);
+ }
+
+ }
}
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
new file mode 100644
index 0000000..22d7717
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java
@@ -0,0 +1,257 @@
+/*
+ * 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.strategy;
+
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Strings;
+
+
+public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrategy {
+ private static final Logger log = LoggerFactory.getLogger(OrderedContentMirrorStoreStrategy.class);
+
+ /**
+ * the property linking to the next node
+ */
+ public static final String NEXT = ":next";
+
+ /**
+ * node that works as root of the index (start point or 0 element)
+ */
+ public static final String START = ":start";
+
+ /**
+ * a NodeState used for easy creating of an empty :start
+ */
+ public static final NodeState EMPTY_START_NODE = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState();
+
+ @Override
+ NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index, @Nonnull String key) {
+ log.debug("fetchKeyNode() - index: {} - key: {}",index,key);
+ NodeBuilder _key = null;
+ NodeBuilder start = index.child(START);
+
+ //identifying the right place for insert
+ String n = start.getString(NEXT);
+ if(Strings.isNullOrEmpty(n)){
+ //new/empty index
+ _key = index.child(key);
+ _key.setProperty(NEXT,"");
+ start.setProperty(NEXT, key);
+ }else{
+ //specific use-case where the item has to be added as first of the list
+ String nextKey = n;
+ if(key.compareTo(nextKey)<0){
+ _key = index.child(key);
+ _key.setProperty(NEXT,nextKey);
+ start.setProperty(NEXT, key);
+ }else{
+ Iterable extends ChildNodeEntry> 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
+ index.getChildNode(child.getName()).setProperty(NEXT, key);
+ _key = index.child(key);
+ _key.setProperty(NEXT, "");
+ } else {
+ if(key.compareTo(nextKey)<0){
+ index.getChildNode(child.getName()).setProperty(NEXT, key);
+ _key = index.child(key);
+ _key.setProperty(NEXT, nextKey);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return _key;
+ }
+
+ @Override
+ void prune(final NodeBuilder index, final Deque builders){
+ for(NodeBuilder node : builders){
+ if(node.hasProperty("match") || node.getChildNodeCount(1) > 0){
+ return;
+ }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
+ if(next==null) {
+ next = "";
+ }
+ index.getChildNode(previous.getName()).setProperty(NEXT, next); //(3) re-link the previous to the next
+ node.remove(); //(4) remove the current node
+ }else{
+ node.remove();
+ }
+ }
+ }
+ }
+
+ @Nullable ChildNodeEntry findPrevious(@Nonnull final NodeState index, @Nonnull final NodeState node){
+ ChildNodeEntry previous = null;
+ ChildNodeEntry current = null;
+ boolean found = false;
+ Iterator extends ChildNodeEntry> it = getChildNodeEntries(index,true).iterator();
+
+ while(!found && it.hasNext()){
+ current = it.next();
+ if(previous==null){
+ //first iteration
+ previous = current;
+ }else{
+ found = node.equals(current.getNodeState());
+ if(!found) {
+ previous = current;
+ }
+ }
+ }
+
+ 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);
+ super.update(index, path, beforeKeys, afterKeys);
+ }
+
+
+ /**
+ * retrieve an Iterable for going through the index in the right order without the :start node
+ *
+ * @param index the root of the index (:index)
+ * @return
+ */
+ @Override
+ @Nonnull Iterable extends ChildNodeEntry> getChildNodeEntries(@Nonnull final NodeState index){
+ return getChildNodeEntries(index, false);
+ }
+
+ /**
+ * Retrieve an Iterable for going through the index in the right order with potentially the :start node
+ *
+ * @param index the root of the index (:index)
+ * @param includeStart true if :start should be included as first element
+ * @return
+ */
+ @Nonnull Iterable extends ChildNodeEntry> getChildNodeEntries(@Nonnull final NodeState index, final boolean includeStart){
+ Iterable extends ChildNodeEntry> cne = null;
+ final NodeState start = index.getChildNode(START);
+
+ if((!start.exists() || Strings.isNullOrEmpty(start.getString(NEXT))) && !includeStart) {
+ //if the property is not there or is empty it means we're empty
+ cne = Collections.emptyList();
+ }else{
+ cne = new Iterable(){
+ private NodeState _index = index;
+ private NodeState _start = ((includeStart && !start.exists())?EMPTY_START_NODE:start);
+ private NodeState current = _start;
+ private boolean _includeStart = includeStart;
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator(){
+
+ @Override
+ public boolean hasNext() {
+ return (
+ (_includeStart && _start.equals(current)) ||
+ (!_includeStart && !Strings.isNullOrEmpty(current.getString(NEXT)))
+ );
+ }
+
+ @Override
+ public ChildNodeEntry next() {
+ ChildNodeEntry _cne = null;
+ if(_includeStart && _start.equals(current)){
+ _cne = new OrderedChildNodeEntry(START, current);
+ _includeStart = false; //let's set it to false. We just included it.
+ } else {
+ if(hasNext()){
+ final String name = current.getString(NEXT);
+ current = _index.getChildNode(name);
+ _cne = new OrderedChildNodeEntry(name, current);
+ }else{
+ throw new NoSuchElementException();
+ }
+ }
+ return _cne;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
+ return cne;
+ }
+
+ private static final class OrderedChildNodeEntry extends AbstractChildNodeEntry {
+ private final String name;
+ private final NodeState state;
+
+ public OrderedChildNodeEntry(@Nonnull final String name,@Nonnull final NodeState state){
+ this.name = name;
+ this.state = state;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.jackrabbit.oak.spi.state.ChildNodeEntry#getName()
+ */
+ @Override
+ @Nonnull
+ public String getName() {
+ return name;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.jackrabbit.oak.spi.state.ChildNodeEntry#getNodeState()
+ */
+ @Override
+ @Nonnull
+ public NodeState getNodeState() {
+ return state;
+ }
+ }
+}
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
new file mode 100644
index 0000000..34f4562
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java
@@ -0,0 +1,67 @@
+package org.apache.jackrabbit.oak.plugins.index.property;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+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.assertTrue;
+
+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.spi.state.NodeBuilder;
+import org.junit.Test;
+
+public class OrderedPropertyIndexEditorTest {
+
+ @Test public void isProperlyConfiguredWithPropertyNames(){
+ NodeBuilder definition = createNiceMock(NodeBuilder.class);
+ PropertyState names = createNiceMock(PropertyState.class);
+ expect(names.count()).andReturn(1);
+ expect(definition.getProperty(IndexConstants.PROPERTY_NAMES)).andReturn(names).anyTimes();
+ replay(names);
+ replay(definition);
+
+ OrderedPropertyIndexEditor ie = new OrderedPropertyIndexEditor(definition, null, null);
+ assertFalse("With empty or missing property the index should not work.",ie.isProperlyConfigured());
+ }
+
+ @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(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());
+ }
+
+ @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(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());
+ }
+}
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
new file mode 100644
index 0000000..5329c4c
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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 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.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;
+ }
+
+ }
+
+ /**
+ * testing for asserting the right comparison behaviour of the custom class
+ */
+ @Test
+ public void valuePathTupleComparison() {
+ try {
+ new ValuePathTuple("value", "path").compareTo(null);
+ fail("It should have raised a NPE");
+ } catch (NullPointerException e) {
+ // so far so good
+ }
+ assertEquals(0, (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value", "path")));
+ assertEquals(-1, (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value1", "path")));
+ assertEquals(-1, (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value1", "path1")));
+ assertEquals(1, (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value", "path")));
+ assertEquals(1, (new ValuePathTuple("value1", "path1")).compareTo(new ValuePathTuple("value1", "path")));
+
+ assertEquals(-1,
+ (new ValuePathTuple("value000", "/test/n1")).compareTo(new ValuePathTuple("value001", "/test/n0")));
+ assertEquals(1,
+ (new ValuePathTuple("value001", "/test/n0")).compareTo(new ValuePathTuple("value000", "/test/n1")));
+ }
+
+ @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)),
+ TEST_INDEX_NAME, false, new String[] { ORDERED_PROPERTY }, null, OrderedIndex.TYPE);
+ root.commit();
+ }
+
+ /**
+ * 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 extends ResultRow> 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
+ * @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), test);
+ root.commit();
+
+ // querying
+ Iterator extends ResultRow> 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);
+ 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));
+ String query = "SELECT * FROM [%s] WHERE %s=$%s";
+ Iterator extends ResultRow> 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());
+ 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/strategy/OrderedContentMirrorStorageStrategyTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java
new file mode 100644
index 0000000..527edcc
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java
@@ -0,0 +1,990 @@
+/*
+ * 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.strategy;
+
+import static com.google.common.collect.Sets.newHashSet;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.NEXT;
+import static org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy.START;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Set;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+
+/**
+ *
+ */
+public class OrderedContentMirrorStorageStrategyTest {
+ /**
+ * ascending ordered set of keys. Useful for testing
+ */
+ private static final String[] KEYS = new String[] { "donald", "goofy", "mickey", "minnie" };
+ private static final Set EMPTY_KEY_SET = newHashSet();
+
+ /**
+ * checks that the fist item/key is inserted with an empty property 'next'
+ *
+ * expected structure:
+ *
+ *
+ * :index : {
+ * :start : { :next=n0 },
+ * n0 : {
+ * :next=,
+ * foo : {
+ * bar: { match=true}
+ * }
+ * }
+ * }
+ *
+ */
+ @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];
+
+ IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+ NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder();
+ NodeBuilder node = null;
+
+ store.update(index, PATH, EMPTY_KEY_SET, newHashSet(N0));
+
+ 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));
+
+ node = index.getChildNode(N0);
+ 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]);
+ assertTrue("n0 should contain 'foo'", node.exists());
+ node = node.getChildNode(PATH_NODES[1]);
+ assertTrue("'foo' should contain 'bar'", node.exists());
+ assertTrue("the 'foo' node should have 'match=true'", node.getBoolean("match"));
+ }
+
+ /**
+ * test the saving of 2 new keys that comes already ordered
+ *
+ * final state of the index will be
+ *
+ *
+ * :index : {
+ * :start : { :next=n0 },
+ * n0 : { :next=n1 },
+ * n1 : { :next= }
+ * }
+ *
+ */
+ @Test
+ public void first2newKeysAlreadyOrdered() {
+ 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
+
+ node = index.getChildNode(START);
+ assertTrue(":index should have :start", node.exists());
+ assertEquals(":start should point to n0", N0, node.getString(NEXT));
+
+ 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
+
+ node = index.getChildNode(START);
+ assertTrue(":index should still have :start", node.exists());
+ assertEquals(":start should still point to n0", N0, node.getString(NEXT));
+
+ node = index.getChildNode(N0);
+ assertTrue("n0 should still exists", node.exists());
+ assertEquals("n0 should point to n1", N1, node.getString(NEXT));
+
+ node = index.getChildNode(N1);
+ assertTrue("n1 should exists", node.exists());
+ assertEquals("n1 should point nowhere", "", node.getString(NEXT));
+ }
+
+ /**
+ * Test the iteration of an empty index
+ */
+ @Test
+ public void childNodeEntriesEmptyIndex() {
+ OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+ NodeState index = EmptyNodeState.EMPTY_NODE;
+
+ @SuppressWarnings("unchecked")
+ Iterable children = (Iterable) store.getChildNodeEntries(index);
+
+ assertNotNull("A returned Iterable cannot be null", children);
+ }
+
+ /**
+ * test the iteration of the index with 2 shuffled items
+ *
+ *
+ * :index : {
+ * :start : { :next=n1 },
+ * n0 : { :next= },
+ * n1 : { :next=n0 }
+ * }
+ *
+ */
+ @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();
+ 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);
+
+ NodeState indexState = index.getNodeState();
+
+ Iterable children = (Iterable) store.getChildNodeEntries(indexState);
+ assertNotNull("The iterable cannot be null", children);
+ assertEquals("Expecting 2 items in the index", 2, Iterators.size(children.iterator())); // how
+ // many
+ // entries
+ // do
+ // we
+ // have?
+
+ // ensuring the right sequence
+ ChildNodeEntry entry = null;
+ children = (Iterable) 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", NODE_1, 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());
+ assertFalse("We should have be at the end of the list", it.hasNext());
+ }
+
+ /**
+ * test the iteration of the index with 2 shuffled items without the :start
+ * node
+ *
+ *
+ * :index : {
+ * :start : { :next=n1 },
+ * n0 : { :next= },
+ * n1 : { :next=n0 }
+ * }
+ *
+ */
+ @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();
+ 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);
+
+ NodeState indexState = index.getNodeState();
+
+ Iterable children = (Iterable) store.getChildNodeEntries(indexState, false);
+ assertNotNull("The iterable cannot be null", children);
+ assertEquals("Expecting 2 items in the index", 2, Iterators.size(children.iterator())); // how
+ // many
+ // entries
+ // do
+ // we
+ // have?
+
+ // ensuring the right sequence
+ ChildNodeEntry entry = null;
+ children = (Iterable) 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", NODE_1, 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());
+ assertFalse("We should have be at the end of the list", it.hasNext());
+ }
+
+ /**
+ * test the iteration of the index with 2 shuffled items including the
+ * :start node as first
+ *
+ *
+ * :index : {
+ * :start : { :next=n1 },
+ * n0 : { :next= },
+ * n1 : { :next=n0 }
+ * }
+ *
+ */
+ @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();
+ 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);
+
+ NodeState indexState = index.getNodeState();
+
+ Iterable children = (Iterable) store.getChildNodeEntries(indexState, true);
+ assertNotNull("The iterable cannot be null", children);
+ assertEquals("Expecting 3 items in the index", 3, Iterators.size(children.iterator())); // how
+ // many
+ // entries
+ // do
+ // we
+ // have?
+
+ // ensuring the right sequence
+ ChildNodeEntry entry = null;
+ children = (Iterable) store.getChildNodeEntries(indexState, true);
+ Iterator it = children.iterator();
+ 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());
+ 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());
+ 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());
+ assertFalse("We should be at the end of the list", it.hasNext());
+ }
+
+ /**
+ * test the iteration over an empty list when the :start is required. In
+ * this case :start should always be returned
+ *
+ *
+ * :index : {
+ * :start : { :next= }
+ * }
+ *
+ */
+ @Test
+ public void childNodeEntriesNoItemsWithStart() {
+ NodeState NODE_START = 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);
+
+ Iterable extends ChildNodeEntry> children = store.getChildNodeEntries(index.getNodeState(), true);
+ assertEquals("Wrong size of Iterable", 1, Iterators.size(children.iterator()));
+
+ Iterator extends ChildNodeEntry> it = store.getChildNodeEntries(index.getNodeState(), true).iterator();
+ 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());
+ assertFalse("We should be at the end of the list", it.hasNext());
+ }
+
+ /**
+ * test the case where we want an iterator for the children of a brand new
+ * index. In this case :start doesn't exists but if we ask for it we should
+ * return it.
+ */
+ @Test
+ public void childNodeEntriesNewIndexWithStart() {
+ NodeState NODE_START = EmptyNodeState.EMPTY_NODE.builder().setProperty(NEXT, "").getNodeState();
+ NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder();
+ OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+
+ Iterator extends ChildNodeEntry> children = store.getChildNodeEntries(index.getNodeState(), true).iterator();
+ assertEquals("Wrong number of children", 1, Iterators.size(children));
+
+ children = store.getChildNodeEntries(index.getNodeState(), true).iterator();
+ assertTrue("at least one item expected", children.hasNext());
+ ChildNodeEntry child = children.next();
+ assertEquals(START, child.getName());
+ assertEquals(NODE_START, child.getNodeState());
+ assertFalse(children.hasNext());
+ }
+
+ /**
+ * test the insert of two shuffled items
+ *
+ * Building final a structure like
+ *
+ *
+ * :index : {
+ * :start : { :next=n1 },
+ * n0 : { :next= },
+ * n1 : { :next=n0 }
+ * }
+ *
+ *
+ * where:
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next =
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n1 },
+ * n0 : {
+ * :next =
+ * },
+ * n1 : {
+ * :next = n0
+ * }
+ * }
+ *
+ */
+ @Test
+ public void twoShuffledItems() {
+ IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+ NodeState root = EmptyNodeState.EMPTY_NODE;
+ NodeBuilder index = root.builder();
+ String key1st = KEYS[1];
+ String key2nd = KEYS[0];
+ NodeState ns = null;
+
+ // Stage 1
+ store.update(index, "/foo/bar", EMPTY_KEY_SET, newHashSet(key1st));
+ ns = index.getChildNode(START).getNodeState();
+ assertEquals(":start is expected to point to the 1st node", key1st, ns.getString(NEXT));
+ ns = index.getChildNode(key1st).getNodeState();
+ assertTrue("At Stage 1 the first node is expected to point nowhere as it's the last",
+ Strings.isNullOrEmpty(ns.getString(NEXT)));
+
+ // Stage 2
+ store.update(index, "/foo/bar", EMPTY_KEY_SET, newHashSet(key2nd));
+ ns = index.getChildNode(START).getNodeState();
+ assertEquals(":start is expected to point to the 2nd node", key2nd, ns.getString(NEXT));
+ ns = index.getChildNode(key1st).getNodeState();
+ assertTrue("At stage 2 the first element should point nowhere as it's the last",
+ Strings.isNullOrEmpty(ns.getString(NEXT)));
+ ns = index.getChildNode(key2nd).getNodeState();
+ assertEquals("At Stage 2 the second element should point to the first one", key1st, ns.getString(NEXT));
+ }
+
+ /**
+ * test the insert of shuffled items
+ *
+ * Building a final structure like
+ *
+ *
+ * {
+ * :start : { :next = n1 },
+ * n0 : {
+ * :next = ""
+ * },
+ * n1 : {
+ * :next = n2
+ * },
+ * n2 : {
+ * :next = n0
+ * }
+ * }
+ *
+ *
+ * where:
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next =
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * {
+ * :start : { :next = n1 },
+ * n0 : { :next = },
+ * n1 : { :next = n0 }
+ * }
+ *
+ * Stage 3
+ * =======
+ *
+ * {
+ * :start : { :next = n1 },
+ * n0 : { :next = },
+ * n1 : { :next = n2 },
+ * n2 : { :next = n0 }
+ * }
+ *
+ * Stage 4
+ * =======
+ *
+ * {
+ * :start : { :next = n1 },
+ * n0 : { :next = n3 },
+ * n1 : { :next = n2 },
+ * n2 : { :next = n0 },
+ * n3 : { :next = }
+ * }
+ *
+ */
+ @Test
+ public void fourShuffledElements() {
+ IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+ NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder();
+ String n0 = KEYS[2];
+ String n1 = KEYS[0];
+ String n2 = KEYS[1];
+ String n3 = KEYS[3];
+
+ // Stage 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)));
+
+ // 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 still be 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 n1", n1, index.getChildNode(START).getString(NEXT));
+ assertEquals("n1 should be pointing to n2", n2, index.getChildNode(n1).getString(NEXT));
+ assertEquals("n2 should be pointing to n0", n0, index.getChildNode(n2).getString(NEXT));
+ assertTrue("n0 should still be the last item of the list",
+ Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT)));
+
+ // Stage 4
+ store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n3));
+ assertEquals(":start should point to n1", n1, index.getChildNode(START).getString(NEXT));
+ assertEquals("n1 should be pointing to n2", n2, index.getChildNode(n1).getString(NEXT));
+ assertEquals("n2 should be pointing to n0", n0, index.getChildNode(n2).getString(NEXT));
+ assertEquals("n0 should be pointing to n3", n3, index.getChildNode(n0).getString(NEXT));
+ assertTrue("n3 should be the last element", Strings.isNullOrEmpty(index.getChildNode(n3).getString(NEXT)));
+ }
+
+ /**
+ * perform a test where the index gets updated if an already existent
+ * node/key gets updated by changing the key and the key contains only 1
+ * item.
+ *
+ * Where the second key is greater than the first.
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start { :next = n0 },
+ * n0 : {
+ * :next =,
+ * content : {
+ * foobar : {
+ * match = true
+ * }
+ * }
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n1 },
+ * n1 : {
+ * :next =,
+ * content : {
+ * foobar : {
+ * match = true
+ * }
+ * }
+ * }
+ * }
+ *
+ */
+
+ @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);
+ IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy();
+ NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder();
+ NodeBuilder node = null;
+
+ // Stage 1
+ 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));
+
+ 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]);
+ assertTrue("n0 should have /content", node.exists());
+
+ 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));
+ node = index.getChildNode(START);
+ 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 = node.getChildNode(NODES[0]);
+ assertTrue("N1 should have /content", node.exists());
+
+ node = node.getChildNode(NODES[1]);
+ assertTrue("/content should contain /foobar", node.exists());
+ assertTrue("/foobar should have match=true", node.getBoolean("match"));
+ }
+
+ /**
+ *
+ * find a previous item given a key in an index with 1 element only
+ *
+ *
+ *
+ * it relies on the functionality of the store.update() for creating the
+ * index
+ *
+ *
+ *
+ * :index {
+ * :start : { :next=n0 },
+ * n0 = { :next= }
+ * }
+ *
+ */
+ @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 NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder();
+
+ index.setChildNode(START, NODE_START);
+ index.setChildNode(N0, NODE_0);
+
+ NodeState indexState = index.getNodeState();
+ ChildNodeEntry previous = store.findPrevious(indexState, NODE_0);
+ assertNotNull(previous);
+ assertEquals("the :start node is expected", NODE_START, previous.getNodeState());
+ }
+
+ /**
+ * test the use case where a document change the indexed property. For
+ * example document that change author.
+ *
+ *
+ * it relies on the functionality of the store.update() for creating the
+ * index
+ *
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next = ,
+ * content : {
+ * one { match=true },
+ * two { match=true }
+ * }
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next = n1,
+ * content : {
+ * one : { match = true }
+ * }
+ * },
+ * n1 : {
+ * :next = ,
+ * content : {
+ * two : { match = true }
+ * }
+ * }
+ * }
+ *
+ */
+ @Test
+ public void documentChangingKey() {
+ 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));
+
+ // ensuring the right structure
+ assertTrue(index.hasChildNode(START));
+ assertTrue(index.hasChildNode(N0));
+ assertFalse(index.hasChildNode(N1));
+
+ NodeBuilder node = index.getChildNode(START);
+ assertEquals(":start pointing to wrong node", N0, 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);
+ 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]);
+ 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));
+ assertTrue(index.hasChildNode(START));
+ assertTrue(index.hasChildNode(N0));
+ assertTrue(index.hasChildNode(N1));
+
+ node = index.getChildNode(START);
+ 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 = 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]));
+
+ 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]);
+ assertTrue(node.exists());
+ assertTrue(node.getBoolean("match"));
+ }
+
+ /**
+ * test when a document is deleted and is the only one under the indexed key
+ *
+ *
+ * it relies on the functionality of the store.update() for creating the
+ * index
+ *
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next = ,
+ * sampledoc : { match = true }
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = }
+ * }
+ *
+ */
+ @Test
+ public void deleteTheOnlyDocument() {
+ 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));
+
+ // 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));
+ assertTrue("as the index should be empty, :start should point nowhere",
+ Strings.isNullOrEmpty(index.getChildNode(START).getString(NEXT)));
+ }
+
+ /**
+ * test when the document is deleted but there're still some documents left
+ * under the indexed key
+ *
+ *
+ * it relies on the functionality of the store.update() for creating the
+ * index
+ *
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next = ,
+ * doc1 : { match=true },
+ * doc2 : { match=true }
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n0 },
+ * n0 : {
+ * :next =,
+ * doc2 : { match = true }
+ * }
+ * }
+ *
+ */
+ @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;
+ 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));
+
+ // 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);
+
+ 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)));
+
+ assertFalse(index.getChildNode(N0).hasChildNode(DOC1));
+ assertTrue(index.getChildNode(N0).hasChildNode(DOC2));
+ assertTrue(index.getChildNode(N0).getChildNode(DOC2).getBoolean("match"));
+ }
+
+ /**
+ * test when the only document is deleted from an indexed key but there're
+ * still some keys left in the index
+ *
+ *
+ * it relies on the functionality of the store.update() for creating the
+ * index
+ *
+ *
+ *
+ * Stage 1
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n1 },
+ * n0 : {
+ * :next = ,
+ * content : {
+ * doc0 : { match = true }
+ * }
+ * },
+ * n1 : {
+ * :next = n2,
+ * content : {
+ * doc1 : { match = true }
+ * }
+ * }
+ * n2 : {
+ * :next = n0,
+ * content : {
+ * doc2 : { match = true }
+ * }
+ * }
+ * }
+ *
+ * Stage 2
+ * =======
+ *
+ * :index : {
+ * :start : { :next = n1 },
+ * n0 : {
+ * :next = ,
+ * content : {
+ * doc0 : { match = true }
+ * }
+ * },
+ * n1 : {
+ * :next = n0,
+ * content : {
+ * doc1 : { match = true }
+ * }
+ * }
+ * }
+ *
+ *
+ */
+ @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];
+
+ 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));
+
+ // 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);
+
+ // checking key nodes
+ assertTrue(index.hasChildNode(START));
+ 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)));
+
+ // 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"));
+
+ 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"));
+ }
+}
diff --git a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java
index 7352cf4..933d0da 100644
--- a/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java
+++ b/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java
@@ -30,6 +30,7 @@ import org.apache.jackrabbit.oak.plugins.commit.ConflictValidatorProvider;
import org.apache.jackrabbit.oak.plugins.commit.JcrConflictHandler;
import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider;
+import org.apache.jackrabbit.oak.plugins.index.property.OrderedPropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexProvider;
import org.apache.jackrabbit.oak.plugins.index.reference.ReferenceEditorProvider;
@@ -79,6 +80,8 @@ public class Jcr {
with(new PropertyIndexProvider());
with(new NodeTypeIndexProvider());
+
+ with(new OrderedPropertyIndexEditorProvider());
}
public Jcr() {
diff --git a/oak-run/pom.xml b/oak-run/pom.xml
index 00c3fcb..ea23235 100644
--- a/oak-run/pom.xml
+++ b/oak-run/pom.xml
@@ -192,6 +192,9 @@
junit
test
+
+ com.google.code.findbugs
+ jsr305
+
-
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java
index 0139b63..54b5ecc 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java
@@ -97,6 +97,13 @@ public class BenchmarkRunner {
base.value(options), 256, cacheSize, mmap.value(options))
};
Benchmark[] allBenchmarks = new Benchmark[] {
+ new OrderedIndexQueryOrderedIndexTest(),
+ new OrderedIndexQueryStandardIndexTest(),
+ new OrderedIndexQueryNoIndexTest(),
+ new OrderedIndexInsertOrderedPropertyTest(),
+ new OrderedIndexInsertStandardPropertyTest(),
+ new OrderedIndexInsertNoIndexTest(),
+ new OrderByQueryTest(),
new LoginTest(),
new LoginLogoutTest(),
new NamespaceTest(),
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java
new file mode 100644
index 0000000..a6cabdb
--- /dev/null
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.benchmark;
+
+import java.util.Random;
+import java.util.UUID;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+import javax.jcr.query.QueryResult;
+
+/**
+ * This benchmark measures the read performance of child nodes using
+ * an ORDER BY query.
+ *
+ * This is related to OAK-1263.
+ *
+ */
+public class OrderByQueryTest extends AbstractTest {
+
+ private static final String NT = "oak:unstructured";
+
+ private static final String ROOT_NODE_NAME = "test" + TEST_ID;
+ private static final int NUM_NODES = 10000;
+ private static final String PROPERTY_NAME = "testProperty";
+ private static final Random random = new Random(); // doesn't have to be very secure, just some randomness
+
+ @Override
+ protected void beforeSuite() throws Exception {
+ Session session = loginWriter();
+ Node rootNode = session.getRootNode();
+ if (rootNode.hasNode(ROOT_NODE_NAME)) {
+ Node root = rootNode.getNode(ROOT_NODE_NAME);
+ root.remove();
+ }
+ rootNode = session.getRootNode().addNode(ROOT_NODE_NAME, NT);
+
+ for (int i = 0; i < NUM_NODES; i++) {
+ if (i%1000==0) {
+ session.save();
+ }
+ Node newNode = rootNode.addNode(UUID.randomUUID().toString(), NT);
+ newNode.setProperty(PROPERTY_NAME, random.nextLong());
+ }
+ session.save();
+ }
+
+ @Override
+ public void runTest() throws Exception {
+ final Session session = loginWriter();
+ try {
+ // run the query
+ final QueryManager qm = session.getWorkspace().getQueryManager();
+
+ final Query q =
+ qm.createQuery("SELECT * FROM [oak:unstructured] AS s WHERE "
+ + "ISDESCENDANTNODE(s, [/"+ROOT_NODE_NAME+"/]) ORDER BY s."+PROPERTY_NAME+"]",
+ Query.JCR_SQL2);
+ final QueryResult res = q.execute();
+
+ final NodeIterator nit = res.getNodes();
+// while(nit.hasNext()) {
+// Node node = nit.nextNode();
+//// System.out.println("node: "+node.getPath()+", prop="+node.getProperty(PROPERTY_NAME).getLong());
+// }
+ } catch (RepositoryException e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java
new file mode 100644
index 0000000..674ab89
--- /dev/null
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.benchmark;
+
+import java.util.UUID;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.oak.benchmark.util.OakIndexUtils;
+import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.property.OrderedPropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
+
+/**
+ *
+ */
+public abstract class OrderedIndexBaseTest extends AbstractTest {
+ /**
+ * the number of nodes created per iteration
+ */
+ static final int NODES_PER_ITERATION = Integer.parseInt(System.getProperty("nodesPerIteration", "100"));
+
+ /**
+ * number of nodes that has to be added before performing the actual test
+ */
+ static final int PRE_ADDED_NODES = Integer.parseInt(System.getProperty("preAddedNodes", "0"));
+
+ /**
+ * type of the created node
+ */
+ static final String NODE_TYPE = NodeTypeConstants.NT_OAK_UNSTRUCTURED;
+
+ /**
+ * property that will be indexed
+ */
+ static final String INDEXED_PROPERTY = "indexedProperty";
+
+ /**
+ * node name below which creating the test data
+ */
+ final String DUMP_NODE = this.getClass().getSimpleName() + TEST_ID;
+
+ /**
+ * session used for operations throughout the test
+ */
+ Session session;
+
+ /**
+ * node under which all the test data will be filled in
+ */
+ Node dump;
+
+ void insertRandomNodes(int numberOfNodes){
+ try{
+ for(int i=0; i
Date: Mon, 3 Mar 2014 12:15:11 +0100
Subject: [PATCH 2/5] OAK-1263 fixing Apache headers
---
.../index/property/OrderedPropertyIndexEditor.java | 17 +++++++++++++++++
.../property/OrderedPropertyIndexEditorProvider.java | 17 +++++++++++++++++
.../index/property/OrderedPropertyIndexEditorTest.java | 17 +++++++++++++++++
3 files changed, 51 insertions(+)
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 46807f7..8a02f2e 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
@@ -1,3 +1,20 @@
+/*
+ * 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.Collections;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorProvider.java
index 75c439d..3ea60ae 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorProvider.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorProvider.java
@@ -1,3 +1,20 @@
+/*
+ * 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 javax.annotation.CheckForNull;
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 34f4562..f75fbdb 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
@@ -1,3 +1,20 @@
+/*
+ * 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 org.easymock.EasyMock.createNiceMock;
--
1.8.3.4 (Apple Git-47)
From 1fec81e8c7eea22eed52342028344894fb7c466f Mon Sep 17 00:00:00 2001
From: Davide Giannella
Date: Mon, 3 Mar 2014 12:15:55 +0100
Subject: [PATCH 3/5] OAK-1263 fixing formatting
---
.../jackrabbit/oak/plugins/index/IndexUtils.java | 208 +++---
.../strategy/ContentMirrorStoreStrategy.java | 725 ++++++++++-----------
2 files changed, 468 insertions(+), 465 deletions(-)
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 ec920c1..4633e9b 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
@@ -49,115 +49,119 @@ import org.apache.jackrabbit.oak.util.NodeUtil;
*/
public class IndexUtils {
- public static NodeBuilder getOrCreateOakIndex(NodeBuilder root) {
- NodeBuilder index;
- if (!root.hasChildNode(INDEX_DEFINITIONS_NAME)) {
- index = root.child(INDEX_DEFINITIONS_NAME);
- // TODO: use property node type name
- index.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
- } else {
- index = root.child(INDEX_DEFINITIONS_NAME);
- }
- return index;
- }
+ public static NodeBuilder getOrCreateOakIndex(NodeBuilder root) {
+ NodeBuilder index;
+ if (!root.hasChildNode(INDEX_DEFINITIONS_NAME)) {
+ index = root.child(INDEX_DEFINITIONS_NAME);
+ // TODO: use property node type name
+ index.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
+ } else {
+ index = root.child(INDEX_DEFINITIONS_NAME);
+ }
+ return index;
+ }
- /**
- * Create a new property index definition below the given {@code indexNode}.
- *
- * @param index The oak:index node builder
- * @param indexDefName The name of the new property index.
- * @param reindex {@code true} if the the reindex flag should be turned on.
- * @param unique {@code true} if the index is expected the assert property
- * uniqueness.
- * @param propertyNames The property names that should be indexed.
- * @param declaringNodeTypeNames The declaring node type names or {@code null}.
- * @return the NodeBuilder of the new index definition.
- */
- public static NodeBuilder createIndexDefinition(@Nonnull NodeBuilder index,
- @Nonnull String indexDefName,
- boolean reindex,
- boolean unique,
- @Nonnull Collection propertyNames,
- @Nullable Collection declaringNodeTypeNames) {
- NodeBuilder entry = index.child(indexDefName)
- .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+ /**
+ * Create a new property index definition below the given {@code indexNode}.
+ *
+ * @param index
+ * The oak:index node builder
+ * @param indexDefName
+ * The name of the new property index.
+ * @param reindex
+ * {@code true} if the the reindex flag should be turned on.
+ * @param unique
+ * {@code true} if the index is expected the assert property uniqueness.
+ * @param propertyNames
+ * The property names that should be indexed.
+ * @param declaringNodeTypeNames
+ * The declaring node type names or {@code null}.
+ * @return the NodeBuilder of the new index definition.
+ */
+ public static NodeBuilder createIndexDefinition(@Nonnull
+ NodeBuilder index, @Nonnull
+ String indexDefName, boolean reindex, boolean unique, @Nonnull
+ Collection propertyNames, @Nullable
+ Collection declaringNodeTypeNames) {
+ NodeBuilder entry = index.child(indexDefName).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
.setProperty(TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE)
.setProperty(REINDEX_PROPERTY_NAME, reindex);
- if (unique) {
- entry.setProperty(UNIQUE_PROPERTY_NAME, unique);
- }
- entry.setProperty(PropertyStates.createProperty(PROPERTY_NAMES, propertyNames, NAMES));
- if (declaringNodeTypeNames != null && !declaringNodeTypeNames.isEmpty()) {
- entry.setProperty(PropertyStates.createProperty(DECLARING_NODE_TYPES, declaringNodeTypeNames, NAMES));
- }
- return entry;
- }
+ if (unique) {
+ entry.setProperty(UNIQUE_PROPERTY_NAME, unique);
+ }
+ entry.setProperty(PropertyStates.createProperty(PROPERTY_NAMES, propertyNames, NAMES));
+ if (declaringNodeTypeNames != null && !declaringNodeTypeNames.isEmpty()) {
+ entry.setProperty(PropertyStates.createProperty(DECLARING_NODE_TYPES, declaringNodeTypeNames, NAMES));
+ }
+ return entry;
+ }
- /**
- * Create a new property2 index definition below the given {@code indexNode}.
- *
- * @param indexNode
- * @param indexDefName
- * @param unique
- * @param propertyNames
- * @param declaringNodeTypeNames
- */
- public static void createIndexDefinition(@Nonnull NodeUtil indexNode,
- @Nonnull String indexDefName,
- boolean unique,
- @Nonnull String[] propertyNames,
- @Nullable String[] declaringNodeTypeNames) throws RepositoryException {
-
- createIndexDefinition(indexNode, indexDefName, unique, propertyNames, declaringNodeTypeNames, PropertyIndexEditorProvider.TYPE);
- }
+ /**
+ * Create a new property2 index definition below the given {@code indexNode}.
+ *
+ * @param indexNode
+ * @param indexDefName
+ * @param unique
+ * @param propertyNames
+ * @param declaringNodeTypeNames
+ */
+ public static void createIndexDefinition(@Nonnull
+ NodeUtil indexNode, @Nonnull
+ String indexDefName, boolean unique, @Nonnull
+ String[] propertyNames, @Nullable
+ String[] declaringNodeTypeNames) throws RepositoryException {
- /**
- * 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
- * @throws RepositoryException
- */
- public static void createIndexDefinition(@Nonnull NodeUtil indexNode,
- @Nonnull String indexDefName,
- boolean unique,
- @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,
+ PropertyIndexEditorProvider.TYPE);
+ }
- public static void createReferenceIndex(@Nonnull NodeBuilder index) {
- index.child(NodeReferenceConstants.NAME)
- .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
- .setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
- }
+ /**
+ * 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
+ * @throws RepositoryException
+ */
+ public static void createIndexDefinition(@Nonnull
+ NodeUtil indexNode, @Nonnull
+ String indexDefName, boolean unique, @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);
+ }
- public static boolean isIndexNodeType(NodeState state) {
- PropertyState ps = state.getProperty(JCR_PRIMARYTYPE);
- return ps != null
- && ps.getValue(STRING).equals(INDEX_DEFINITIONS_NODE_TYPE);
- }
+ public static void createReferenceIndex(@Nonnull
+ NodeBuilder index) {
+ index.child(NodeReferenceConstants.NAME).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+ .setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
+ }
- public static boolean isIndexNodeType(NodeState state, String typeIn) {
- if (!isIndexNodeType(state)) {
- return false;
- }
- PropertyState type = state.getProperty(TYPE_PROPERTY_NAME);
- return type != null && !type.isArray()
- && type.getValue(Type.STRING).equals(typeIn);
- }
+ public static boolean isIndexNodeType(NodeState state) {
+ PropertyState ps = state.getProperty(JCR_PRIMARYTYPE);
+ return ps != null && ps.getValue(STRING).equals(INDEX_DEFINITIONS_NODE_TYPE);
+ }
+
+ public static boolean isIndexNodeType(NodeState state, String typeIn) {
+ if (!isIndexNodeType(state)) {
+ return false;
+ }
+ PropertyState type = state.getProperty(TYPE_PROPERTY_NAME);
+ return type != null && !type.isArray() && type.getValue(Type.STRING).equals(typeIn);
+ }
}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
index 95b640c..9cc2a84 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ContentMirrorStoreStrategy.java
@@ -44,14 +44,12 @@ import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
/**
- * An IndexStoreStrategy implementation that saves the nodes under a hierarchy
- * that mirrors the repository tree.
- * This should minimize the chance that concurrent updates overlap on the same
- * content node.
+ * An IndexStoreStrategy implementation that saves the nodes under a hierarchy that mirrors the repository tree.
+ * This should minimize the chance that concurrent updates overlap on the same content node.
*
- * For example for a node that is under {@code /test/node}, the index
- * structure will be {@code /oak:index/index/test/node}:
- *
+ * For example for a node that is under {@code /test/node}, the index structure will be
+ * {@code /oak:index/index/test/node}:
+ *
*
* {@code
* /
@@ -63,374 +61,375 @@ import com.google.common.collect.Sets;
* node
* }
*
- *
+ *
*/
public class ContentMirrorStoreStrategy implements IndexStoreStrategy {
- static final Logger LOG = LoggerFactory.getLogger(ContentMirrorStoreStrategy.class);
-
- @Override
- public void update(
- NodeBuilder index, String path,
- Set beforeKeys, Set afterKeys) {
- for (String key : beforeKeys) {
- remove(index, key, path);
- }
- for (String key : afterKeys) {
- insert(index, key, path);
- }
- }
-
- private void remove(NodeBuilder index, String key, String value) {
- NodeBuilder builder = index.getChildNode(key);
- if (builder.exists()) {
- // Collect all builders along the given path
- Deque builders = newArrayDeque();
- builders.addFirst(builder);
-
- // Descend to the correct location in the index tree
- for (String name : PathUtils.elements(value)) {
- builder = builder.getChildNode(name);
+ static final Logger LOG = LoggerFactory.getLogger(ContentMirrorStoreStrategy.class);
+
+ @Override
+ public void update(NodeBuilder index, String path, Set beforeKeys, Set afterKeys) {
+ for (String key : beforeKeys) {
+ remove(index, key, path);
+ }
+ for (String key : afterKeys) {
+ insert(index, key, path);
+ }
+ }
+
+ private void remove(NodeBuilder index, String key, String value) {
+ NodeBuilder builder = index.getChildNode(key);
+ if (builder.exists()) {
+ // Collect all builders along the given path
+ Deque builders = newArrayDeque();
builders.addFirst(builder);
- }
-
- // Drop the match value, if present
- if (builder.exists()) {
- builder.removeProperty("match");
- }
-
- // Prune all index nodes that are no longer needed
- prune(index, builders);
- }
- }
-
- /**
- * Physically prune a list of nodes from the index
- *
- * @param index the current index
- * @param builders list of nodes to prune
- */
- void prune(final NodeBuilder index, final Deque builders){
- for (NodeBuilder node : builders) {
- if (node.getBoolean("match") || node.getChildNodeCount(1) > 0) {
- return;
- } else if (node.exists()) {
- node.remove();
- }
- }
- }
-
- private void insert(NodeBuilder index, String key, String value) {
- // NodeBuilder builder = index.child(key);
- NodeBuilder builder = fetchKeyNode(index, key);
- for (String name : PathUtils.elements(value)) {
- builder = builder.child(name);
- }
- builder.setProperty("match", true);
- }
-
- /**
- * fetch from the index the key node
- *
- * @param index the current index root
- * @param key the 'key' to fetch from the repo
- * @return the node representing the key
- */
- NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index,@Nonnull String key){
- return index.child(key);
- }
-
- public Iterable query(final Filter filter, final String indexName,
- final NodeState indexMeta, final String indexStorageNodeName,
- final Iterable values) {
- final NodeState index = indexMeta.getChildNode(indexStorageNodeName);
- return new Iterable() {
- @Override
- public Iterator iterator() {
- PathIterator it = new PathIterator(filter, indexName);
- if (values == null) {
- it.setPathContainsValue(true);
- it.enqueue(getChildNodeEntries(index).iterator());
- } else {
- for (String p : values) {
- NodeState property = index.getChildNode(p);
- if (property.exists()) {
- // we have an entry for this value, so use it
- it.enqueue(Iterators.singletonIterator(
- new MemoryChildNodeEntry("", property)));
- }
- }
+
+ // Descend to the correct location in the index tree
+ for (String name : PathUtils.elements(value)) {
+ builder = builder.getChildNode(name);
+ builders.addFirst(builder);
}
- return it;
- }
- };
- }
-
- @Nonnull Iterable extends ChildNodeEntry> getChildNodeEntries(@Nonnull final NodeState index){
- return index.getChildNodeEntries();
- }
-
- @Override
- public Iterable query(final Filter filter, final String indexName,
- final NodeState indexMeta, final Iterable values) {
- return query(filter, indexName, indexMeta, INDEX_CONTENT_NODE_NAME, values);
- }
-
- @Override
- public long count(NodeState indexMeta, Set values, int max) {
- return count(indexMeta, INDEX_CONTENT_NODE_NAME, values, max);
- }
-
- public long count(NodeState indexMeta, final String indexStorageNodeName,
- Set values, int max) {
- NodeState index = indexMeta.getChildNode(indexStorageNodeName);
- int count = 0;
- if (values == null) {
- PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME);
- if (ec != null) {
- return ec.getValue(Type.LONG);
- }
- CountingNodeVisitor v = new CountingNodeVisitor(max);
- v.visit(index);
- count = v.getEstimatedCount();
- // "is not null" queries typically read more data
- count *= 10;
- } else {
- int size = values.size();
- if (size == 0) {
- return 0;
- }
- max = Math.max(10, max / size);
- int i = 0;
- for (String p : values) {
- if (count > max && i > 3) {
- // the total count is extrapolated from the the number
- // of values counted so far to the total number of values
- count = count * size / i;
- break;
+
+ // Drop the match value, if present
+ if (builder.exists()) {
+ builder.removeProperty("match");
}
- NodeState s = index.getChildNode(p);
- if (s.exists()) {
- CountingNodeVisitor v = new CountingNodeVisitor(max);
- v.visit(s);
- count += v.getEstimatedCount();
+
+ // Prune all index nodes that are no longer needed
+ prune(index, builders);
+ }
+ }
+
+ /**
+ * Physically prune a list of nodes from the index
+ *
+ * @param index
+ * the current index
+ * @param builders
+ * list of nodes to prune
+ */
+ void prune(final NodeBuilder index, final Deque builders) {
+ for (NodeBuilder node : builders) {
+ if (node.getBoolean("match") || node.getChildNodeCount(1) > 0) {
+ return;
+ } else if (node.exists()) {
+ node.remove();
}
- i++;
- }
- }
- return count;
- }
-
- /**
- * An iterator over paths within an index node.
- */
- static class PathIterator implements Iterator {
-
- private final Filter filter;
- private final String indexName;
- private final Deque> nodeIterators =
- Queues.newArrayDeque();
- private int readCount;
- private boolean init;
- private boolean closed;
- private String parentPath;
- private String currentPath;
- private boolean pathContainsValue;
-
- /**
- * Keep the returned path, to avoid returning duplicate entries.
- */
- private final Set knownPaths = Sets.newHashSet();
-
- PathIterator(Filter filter, String indexName) {
- this.filter = filter;
- this.indexName = indexName;
- parentPath = "";
- currentPath = "/";
- }
-
- void enqueue(Iterator extends ChildNodeEntry> it) {
- nodeIterators.addLast(it);
- }
-
- void setPathContainsValue(boolean pathContainsValue) {
- if (init) {
- throw new IllegalStateException("This iterator is already initialized");
- }
- this.pathContainsValue = pathContainsValue;
- }
-
- @Override
- public boolean hasNext() {
- if (!closed && !init) {
- fetchNext();
- init = true;
- }
- return !closed;
- }
-
- private void fetchNext() {
- while (true) {
- fetchNextPossiblyDuplicate();
- if (closed) {
- return;
+ }
+ }
+
+ private void insert(NodeBuilder index, String key, String value) {
+ // NodeBuilder builder = index.child(key);
+ NodeBuilder builder = fetchKeyNode(index, key);
+ for (String name : PathUtils.elements(value)) {
+ builder = builder.child(name);
+ }
+ builder.setProperty("match", true);
+ }
+
+ /**
+ * fetch from the index the key node
+ *
+ * @param index
+ * the current index root
+ * @param key
+ * the 'key' to fetch from the repo
+ * @return the node representing the key
+ */
+ NodeBuilder fetchKeyNode(@Nonnull
+ NodeBuilder index, @Nonnull
+ String key) {
+ return index.child(key);
+ }
+
+ public Iterable query(final Filter filter, final String indexName, final NodeState indexMeta,
+ final String indexStorageNodeName, final Iterable values) {
+ final NodeState index = indexMeta.getChildNode(indexStorageNodeName);
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ PathIterator it = new PathIterator(filter, indexName);
+ if (values == null) {
+ it.setPathContainsValue(true);
+ it.enqueue(getChildNodeEntries(index).iterator());
+ } else {
+ for (String p : values) {
+ NodeState property = index.getChildNode(p);
+ if (property.exists()) {
+ // we have an entry for this value, so use it
+ it.enqueue(Iterators.singletonIterator(new MemoryChildNodeEntry("", property)));
+ }
+ }
+ }
+ return it;
+ }
+ };
+ }
+
+ @Nonnull
+ Iterable extends ChildNodeEntry> getChildNodeEntries(@Nonnull
+ final NodeState index) {
+ return index.getChildNodeEntries();
+ }
+
+ @Override
+ public Iterable query(final Filter filter, final String indexName, final NodeState indexMeta,
+ final Iterable values) {
+ return query(filter, indexName, indexMeta, INDEX_CONTENT_NODE_NAME, values);
+ }
+
+ @Override
+ public long count(NodeState indexMeta, Set values, int max) {
+ return count(indexMeta, INDEX_CONTENT_NODE_NAME, values, max);
+ }
+
+ public long count(NodeState indexMeta, final String indexStorageNodeName, Set values, int max) {
+ NodeState index = indexMeta.getChildNode(indexStorageNodeName);
+ int count = 0;
+ if (values == null) {
+ PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME);
+ if (ec != null) {
+ return ec.getValue(Type.LONG);
+ }
+ CountingNodeVisitor v = new CountingNodeVisitor(max);
+ v.visit(index);
+ count = v.getEstimatedCount();
+ // "is not null" queries typically read more data
+ count *= 10;
+ } else {
+ int size = values.size();
+ if (size == 0) {
+ return 0;
}
- if (pathContainsValue) {
- String value = PathUtils.elements(currentPath).iterator().next();
- currentPath = PathUtils.relativize(value, currentPath);
- // don't return duplicate paths:
- // Set.add returns true if the entry was new,
- // so if it returns false, it was already known
- if (!knownPaths.add(currentPath)) {
- continue;
- }
+ max = Math.max(10, max / size);
+ int i = 0;
+ for (String p : values) {
+ if (count > max && i > 3) {
+ // the total count is extrapolated from the the number
+ // of values counted so far to the total number of values
+ count = count * size / i;
+ break;
+ }
+ NodeState s = index.getChildNode(p);
+ if (s.exists()) {
+ CountingNodeVisitor v = new CountingNodeVisitor(max);
+ v.visit(s);
+ count += v.getEstimatedCount();
+ }
+ i++;
}
- break;
- }
- }
-
- private void fetchNextPossiblyDuplicate() {
- while (!nodeIterators.isEmpty()) {
- Iterator extends ChildNodeEntry> iterator = nodeIterators.getLast();
- if (iterator.hasNext()) {
- ChildNodeEntry entry = iterator.next();
-
- readCount++;
- if (readCount % 1000 == 0) {
- FilterIterators.checkReadLimit(readCount);
- LOG.warn("Traversed " + readCount + " nodes using index " + indexName + " with filter " + filter);
- }
-
- NodeState node = entry.getNodeState();
-
- String name = entry.getName();
- if (NodeStateUtils.isHidden(name)) {
- continue;
- }
- currentPath = PathUtils.concat(parentPath, name);
-
- nodeIterators.addLast(node.getChildNodeEntries().iterator());
- parentPath = currentPath;
-
- if (node.getBoolean("match")) {
- return;
- }
-
- } else {
- nodeIterators.removeLast();
- parentPath = PathUtils.getParentPath(parentPath);
+ }
+ return count;
+ }
+
+ /**
+ * An iterator over paths within an index node.
+ */
+ static class PathIterator implements Iterator {
+
+ private final Filter filter;
+ private final String indexName;
+ private final Deque> nodeIterators = Queues.newArrayDeque();
+ private int readCount;
+ private boolean init;
+ private boolean closed;
+ private String parentPath;
+ private String currentPath;
+ private boolean pathContainsValue;
+
+ /**
+ * Keep the returned path, to avoid returning duplicate entries.
+ */
+ private final Set knownPaths = Sets.newHashSet();
+
+ PathIterator(Filter filter, String indexName) {
+ this.filter = filter;
+ this.indexName = indexName;
+ parentPath = "";
+ currentPath = "/";
+ }
+
+ void enqueue(Iterator extends ChildNodeEntry> it) {
+ nodeIterators.addLast(it);
+ }
+
+ void setPathContainsValue(boolean pathContainsValue) {
+ if (init) {
+ throw new IllegalStateException("This iterator is already initialized");
}
- }
- currentPath = null;
- closed = true;
- }
-
- @Override
- public String next() {
- if (closed) {
- throw new IllegalStateException("This iterator is closed");
- }
- if (!init) {
+ this.pathContainsValue = pathContainsValue;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!closed && !init) {
+ fetchNext();
+ init = true;
+ }
+ return !closed;
+ }
+
+ private void fetchNext() {
+ while (true) {
+ fetchNextPossiblyDuplicate();
+ if (closed) {
+ return;
+ }
+ if (pathContainsValue) {
+ String value = PathUtils.elements(currentPath).iterator().next();
+ currentPath = PathUtils.relativize(value, currentPath);
+ // don't return duplicate paths:
+ // Set.add returns true if the entry was new,
+ // so if it returns false, it was already known
+ if (!knownPaths.add(currentPath)) {
+ continue;
+ }
+ }
+ break;
+ }
+ }
+
+ private void fetchNextPossiblyDuplicate() {
+ while (!nodeIterators.isEmpty()) {
+ Iterator extends ChildNodeEntry> iterator = nodeIterators.getLast();
+ if (iterator.hasNext()) {
+ ChildNodeEntry entry = iterator.next();
+
+ readCount++;
+ if (readCount % 1000 == 0) {
+ FilterIterators.checkReadLimit(readCount);
+ LOG.warn("Traversed " + readCount + " nodes using index " + indexName + " with filter "
+ + filter);
+ }
+
+ NodeState node = entry.getNodeState();
+
+ String name = entry.getName();
+ if (NodeStateUtils.isHidden(name)) {
+ continue;
+ }
+ currentPath = PathUtils.concat(parentPath, name);
+
+ nodeIterators.addLast(node.getChildNodeEntries().iterator());
+ parentPath = currentPath;
+
+ if (node.getBoolean("match")) {
+ return;
+ }
+
+ } else {
+ nodeIterators.removeLast();
+ parentPath = PathUtils.getParentPath(parentPath);
+ }
+ }
+ currentPath = null;
+ closed = true;
+ }
+
+ @Override
+ public String next() {
+ if (closed) {
+ throw new IllegalStateException("This iterator is closed");
+ }
+ if (!init) {
+ fetchNext();
+ init = true;
+ }
+ String result = currentPath;
fetchNext();
- init = true;
- }
- String result = currentPath;
- fetchNext();
- return result;
- }
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
-
- }
-
- /**
- * A node visitor to recursively traverse a number of nodes.
- */
- interface NodeVisitor {
- void visit(NodeState state);
- }
-
- /**
- * A node visitor that counts the number of matching nodes up to a given
- * maximum, in order to estimate the number of matches.
- */
- static class CountingNodeVisitor implements NodeVisitor {
-
- /**
- * The maximum number of matching nodes to count.
- */
- final int maxCount;
-
- /**
- * The current count of matching nodes.
- */
- int count;
-
- /**
- * The current depth (number of parent nodes).
- */
- int depth;
-
- /**
- * The sum of the depth of all matching nodes. This value is used to
- * calculate the average depth.
- */
- long depthTotal;
-
- CountingNodeVisitor(int maxCount) {
- this.maxCount = maxCount;
- }
-
- @Override
- public void visit(NodeState state) {
- if (state.hasProperty("match")) {
- count++;
- depthTotal += depth;
- }
- if (count < maxCount) {
- depth++;
- for (ChildNodeEntry entry : state.getChildNodeEntries()) {
- if (count >= maxCount) {
- break;
- }
- visit(entry.getNodeState());
+ return result;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * A node visitor to recursively traverse a number of nodes.
+ */
+ interface NodeVisitor {
+ void visit(NodeState state);
+ }
+
+ /**
+ * A node visitor that counts the number of matching nodes up to a given maximum, in order to estimate the number of
+ * matches.
+ */
+ static class CountingNodeVisitor implements NodeVisitor {
+
+ /**
+ * The maximum number of matching nodes to count.
+ */
+ final int maxCount;
+
+ /**
+ * The current count of matching nodes.
+ */
+ int count;
+
+ /**
+ * The current depth (number of parent nodes).
+ */
+ int depth;
+
+ /**
+ * The sum of the depth of all matching nodes. This value is used to calculate the average depth.
+ */
+ long depthTotal;
+
+ CountingNodeVisitor(int maxCount) {
+ this.maxCount = maxCount;
+ }
+
+ @Override
+ public void visit(NodeState state) {
+ if (state.hasProperty("match")) {
+ count++;
+ depthTotal += depth;
}
- depth--;
- }
- }
-
- /**
- * The number of matches (at most the maximum count).
- *
- * @return the match count
- */
- int getCount() {
- return count;
- }
-
- /**
- * The number of estimated matches. This value might be higher than the
- * number of counted matches, if the maximum number of matches has been
- * reached. It is based on the average depth of matches, and the average
- * number of child nodes.
- *
- * @return the estimated matches
- */
- int getEstimatedCount() {
- if (count < maxCount) {
+ if (count < maxCount) {
+ depth++;
+ for (ChildNodeEntry entry : state.getChildNodeEntries()) {
+ if (count >= maxCount) {
+ break;
+ }
+ visit(entry.getNodeState());
+ }
+ depth--;
+ }
+ }
+
+ /**
+ * The number of matches (at most the maximum count).
+ *
+ * @return the match count
+ */
+ int getCount() {
return count;
- }
- double averageDepth = (int) (depthTotal / count);
- // the number of estimated matches is higher
- // the higher the average depth of the first hits
- long estimatedNodes = (long) (count * Math.pow(1.1, averageDepth));
- estimatedNodes = Math.min(estimatedNodes, Integer.MAX_VALUE);
- return Math.max(count, (int) estimatedNodes);
- }
-
- }
+ }
+
+ /**
+ * The number of estimated matches. This value might be higher than the number of counted matches, if the
+ * maximum number of matches has been reached. It is based on the average depth of matches, and the average
+ * number of child nodes.
+ *
+ * @return the estimated matches
+ */
+ int getEstimatedCount() {
+ if (count < maxCount) {
+ return count;
+ }
+ double averageDepth = (int) (depthTotal / count);
+ // the number of estimated matches is higher
+ // the higher the average depth of the first hits
+ long estimatedNodes = (long) (count * Math.pow(1.1, averageDepth));
+ estimatedNodes = Math.min(estimatedNodes, Integer.MAX_VALUE);
+ return Math.max(count, (int) estimatedNodes);
+ }
+
+ }
}
--
1.8.3.4 (Apple Git-47)
From 8cd39408e41885a3d7770deff1ebf65c822657fb Mon Sep 17 00:00:00 2001
From: Davide Giannella
Date: Mon, 3 Mar 2014 12:33:52 +0100
Subject: [PATCH 4/5] OAK-1263 fixing formatting
---
.../jackrabbit/oak/plugins/index/IndexUtils.java | 64 +++++++++-------------
1 file changed, 27 insertions(+), 37 deletions(-)
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 4633e9b..bcc394f 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
@@ -64,28 +64,21 @@ public class IndexUtils {
/**
* Create a new property index definition below the given {@code indexNode}.
*
- * @param index
- * The oak:index node builder
- * @param indexDefName
- * The name of the new property index.
- * @param reindex
- * {@code true} if the the reindex flag should be turned on.
- * @param unique
- * {@code true} if the index is expected the assert property uniqueness.
- * @param propertyNames
- * The property names that should be indexed.
- * @param declaringNodeTypeNames
- * The declaring node type names or {@code null}.
+ * @param index The oak:index node builder
+ * @param indexDefName The name of the new property index.
+ * @param reindex {@code true} if the the reindex flag should be turned on.
+ * @param unique {@code true} if the index is expected the assert property uniqueness.
+ * @param propertyNames The property names that should be indexed.
+ * @param declaringNodeTypeNames The declaring node type names or {@code null}.
* @return the NodeBuilder of the new index definition.
*/
- public static NodeBuilder createIndexDefinition(@Nonnull
- NodeBuilder index, @Nonnull
- String indexDefName, boolean reindex, boolean unique, @Nonnull
- Collection propertyNames, @Nullable
- Collection declaringNodeTypeNames) {
- NodeBuilder entry = index.child(indexDefName).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
- .setProperty(TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE)
- .setProperty(REINDEX_PROPERTY_NAME, reindex);
+ public static NodeBuilder createIndexDefinition(@Nonnull NodeBuilder index,
+ @Nonnull String indexDefName,
+ boolean reindex,
+ boolean unique,
+ @Nonnull Collection propertyNames,
+ @Nullable Collection declaringNodeTypeNames) {
+ NodeBuilder entry = index.child(indexDefName).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME).setProperty(TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE).setProperty(REINDEX_PROPERTY_NAME, reindex);
if (unique) {
entry.setProperty(UNIQUE_PROPERTY_NAME, unique);
}
@@ -105,19 +98,17 @@ public class IndexUtils {
* @param propertyNames
* @param declaringNodeTypeNames
*/
- public static void createIndexDefinition(@Nonnull
- NodeUtil indexNode, @Nonnull
- String indexDefName, boolean unique, @Nonnull
- String[] propertyNames, @Nullable
- String[] declaringNodeTypeNames) throws RepositoryException {
+ public static void createIndexDefinition(@Nonnull NodeUtil indexNode,
+ @Nonnull String indexDefName,
+ boolean unique,
+ @Nonnull String[] propertyNames,
+ @Nullable String[] declaringNodeTypeNames) throws RepositoryException {
- createIndexDefinition(indexNode, indexDefName, unique, propertyNames, declaringNodeTypeNames,
- PropertyIndexEditorProvider.TYPE);
+ createIndexDefinition(indexNode, indexDefName, unique, propertyNames, declaringNodeTypeNames, PropertyIndexEditorProvider.TYPE);
}
/**
- * Create a new property index definition below the given {@code indexNode} of the provided
- * {@code propertyIndexType}.
+ * Create a new property index definition below the given {@code indexNode} of the provided {@code propertyIndexType}.
*
* @param indexNode
* @param indexDefName
@@ -127,12 +118,12 @@ public class IndexUtils {
* @param propertyIndexType
* @throws RepositoryException
*/
- public static void createIndexDefinition(@Nonnull
- NodeUtil indexNode, @Nonnull
- String indexDefName, boolean unique, @Nonnull
- String[] propertyNames, @Nullable
- String[] declaringNodeTypeNames, @Nonnull
- String propertyIndexType) throws RepositoryException {
+ public static void createIndexDefinition(@Nonnull NodeUtil indexNode,
+ @Nonnull String indexDefName,
+ boolean unique,
+ @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);
@@ -147,8 +138,7 @@ public class IndexUtils {
public static void createReferenceIndex(@Nonnull
NodeBuilder index) {
- index.child(NodeReferenceConstants.NAME).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
- .setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
+ index.child(NodeReferenceConstants.NAME).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME).setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
}
public static boolean isIndexNodeType(NodeState state) {
--
1.8.3.4 (Apple Git-47)
From 1c5627a1c897211ced21147d9c3c27e6a4d4fb12 Mon Sep 17 00:00:00 2001
From: Davide Giannella
Date: Mon, 3 Mar 2014 12:56:35 +0100
Subject: [PATCH 5/5] OAK-1263 fixing formatting
---
.../jackrabbit/oak/plugins/index/IndexUtils.java | 18 ++++++++++++------
.../oak/plugins/index/property/PropertyIndex.java | 11 ++++-------
2 files changed, 16 insertions(+), 13 deletions(-)
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 bcc394f..25cc7c9 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
@@ -78,7 +78,10 @@ public class IndexUtils {
boolean unique,
@Nonnull Collection propertyNames,
@Nullable Collection declaringNodeTypeNames) {
- NodeBuilder entry = index.child(indexDefName).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME).setProperty(TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE).setProperty(REINDEX_PROPERTY_NAME, reindex);
+ NodeBuilder entry = index.child(indexDefName)
+ .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+ .setProperty(TYPE_PROPERTY_NAME, PropertyIndexEditorProvider.TYPE)
+ .setProperty(REINDEX_PROPERTY_NAME, reindex);
if (unique) {
entry.setProperty(UNIQUE_PROPERTY_NAME, unique);
}
@@ -136,14 +139,16 @@ public class IndexUtils {
entry.setNames(PROPERTY_NAMES, propertyNames);
}
- public static void createReferenceIndex(@Nonnull
- NodeBuilder index) {
- index.child(NodeReferenceConstants.NAME).setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME).setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
+ public static void createReferenceIndex(@Nonnull NodeBuilder index) {
+ index.child(NodeReferenceConstants.NAME)
+ .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+ .setProperty(TYPE_PROPERTY_NAME, NodeReferenceConstants.TYPE);
}
public static boolean isIndexNodeType(NodeState state) {
PropertyState ps = state.getProperty(JCR_PRIMARYTYPE);
- return ps != null && ps.getValue(STRING).equals(INDEX_DEFINITIONS_NODE_TYPE);
+ return ps != null
+ && ps.getValue(STRING).equals(INDEX_DEFINITIONS_NODE_TYPE);
}
public static boolean isIndexNodeType(NodeState state, String typeIn) {
@@ -151,7 +156,8 @@ public class IndexUtils {
return false;
}
PropertyState type = state.getProperty(TYPE_PROPERTY_NAME);
- return type != null && !type.isArray() && type.getValue(Type.STRING).equals(typeIn);
+ return type != null && !type.isArray()
+ && type.getValue(Type.STRING).equals(typeIn);
}
}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java
index dd36ba6..bde1035 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java
@@ -50,15 +50,13 @@ import com.google.common.collect.Iterables;
* Optionally you can specify
*
* - a uniqueness constraint on a property index by setting the
unique flag to true
- * - that the property index only applies to a certain node type by setting the
declaringNodeTypes
- * property
+ * - that the property index only applies to a certain node type by setting the
declaringNodeTypes property
*
*
*
* Notes:
*
- * -
propertyNames can be a list of properties, and it is optional.in case it is missing, the node name
- * will be used as a property name reference value
+ * -
propertyNames can be a list of properties, and it is optional.in case it is missing, the node name will be used as a property name reference value
* -
reindex is a property that when set to true, triggers a full content reindex.
*
*
@@ -114,7 +112,7 @@ class PropertyIndex implements QueryIndex {
return values;
}
- // --------------------------------------------------------< QueryIndex >--
+ //--------------------------------------------------------< QueryIndex >--
@Override
public String getIndexName() {
@@ -198,8 +196,7 @@ class PropertyIndex implements QueryIndex {
}
}
if (paths == null) {
- throw new IllegalStateException("Property index is used even when no index is available for filter "
- + filter);
+ throw new IllegalStateException("Property index is used even when no index is available for filter " + filter);
}
Cursor c = Cursors.newPathCursor(paths);
if (depth > 1) {
--
1.8.3.4 (Apple Git-47)