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 7c4e19f..228006b 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 @@ -107,8 +107,29 @@ public class IndexUtils { boolean unique, @Nonnull String[] propertyNames, @Nullable String[] declaringNodeTypeNames) throws RepositoryException { + + createIndexDefinition(indexNode, indexDefName, unique, propertyNames, declaringNodeTypeNames, PropertyIndexEditorProvider.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, PropertyIndexEditorProvider.TYPE); + entry.setString(TYPE_PROPERTY_NAME, propertyIndexType); entry.setBoolean(REINDEX_PROPERTY_NAME, true); if (unique) { entry.setBoolean(UNIQUE_PROPERTY_NAME, true); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java new file mode 100644 index 0000000..e45a1db --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * interface for shared constants around different actors: QueryIndex, IndexEditors, + * IndexEditorProviders, ... + */ +public interface OrderedIndex { + String TYPE = "ordered"; +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java new file mode 100644 index 0000000..0fb6c35 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java @@ -0,0 +1,35 @@ +/* + * 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.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.TYPE; + +import org.apache.jackrabbit.oak.spi.state.NodeState; + +public class OrderedPropertyIndex extends PropertyIndex { + + @Override + public String getIndexName() { + return TYPE; + } + + @Override + PropertyIndexLookup getLookup(NodeState root) { + return new OrderedPropertyIndexLookup(root); + } +} 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 new file mode 100644 index 0000000..36ff888 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditor.java @@ -0,0 +1,150 @@ +/* + * 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; +import java.util.Set; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback; +import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy; +import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Strings; + +public class OrderedPropertyIndexEditor extends PropertyIndexEditor { + private static final Logger log = LoggerFactory.getLogger(OrderedPropertyIndexEditor.class); + private static final IndexStoreStrategy ORDERED_MIRROR = new OrderedContentMirrorStoreStrategy(); + + private final Set propertyNames; + + private boolean properlyConfigured; + + public OrderedPropertyIndexEditor(NodeBuilder definition, NodeState root, + IndexUpdateCallback callback) { + super(definition, root, callback); + + Set pns = null; + + PropertyState names = definition.getProperty(IndexConstants.PROPERTY_NAMES); + if (names != null) { + String value = names.getValue(Type.NAME, 0); + if (Strings.isNullOrEmpty(value)) { + log.warn("Empty value passed as propertyNames. Index not properly configured. Ignoring."); + } else { + if (names.isArray()) { + log.warn("Only single value supported. '{}' only will be used.", value); + } + pns = Collections.singleton(value); + this.properlyConfigured = true; + } + } + + this.propertyNames = pns; + } + + OrderedPropertyIndexEditor(OrderedPropertyIndexEditor parent, String name) { + super(parent, name); + this.propertyNames = parent.getPropertyNames(); + } + + /** + * Same as {@link PropertyIndexEditor#getStrategy(boolean)} but ignores the boolean flag. + * + * @return the proper index strategy + */ + @Override + IndexStoreStrategy getStrategy(boolean unique) { + return ORDERED_MIRROR; + } + + public boolean isProperlyConfigured() { + return properlyConfigured; + } + + @Override + Set getPropertyNames() { + return propertyNames; + } + + @Override + PropertyIndexEditor getChildIndexEditor(@Nonnull PropertyIndexEditor parent, + @Nonnull String name) { + return new OrderedPropertyIndexEditor(this, name); + } + + @Override + public void enter(NodeState before, NodeState after) { + log.debug("enter() - before: {} - after: {}", before, after); + super.enter(before, after); + } + + @Override + public void leave(NodeState before, NodeState after) throws CommitFailedException { + log.debug("leave() - before: {} - after: {}", before, after); + super.leave(before, after); + } + + @Override + public void propertyAdded(PropertyState after) { + log.debug("propertyAdded() - after: {}", after); + super.propertyAdded(after); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + log.debug("propertyChanged() - before: {} - after: {}", before, after); + super.propertyChanged(before, after); + } + + @Override + public void propertyDeleted(PropertyState before) { + log.debug("propertyDeleted() - before: {}", before); + super.propertyDeleted(before); + } + + @Override + public Editor childNodeAdded(String name, NodeState after) { + log.debug("childNodeAdded() - name: {} - after: {}", name, after); + return super.childNodeAdded(name, after); + } + + @Override + public Editor childNodeChanged(String name, NodeState before, NodeState after) { + log.debug("childNodeChanged() - name: {} - before: {} - after: {}", new Object[] { name, + before, + after }); + return super.childNodeChanged(name, before, after); + } + + @Override + public Editor childNodeDeleted(String name, NodeState before) { + log.debug("childNodeDeleted() - name: {} - before: {}", name, before); + return super.childNodeDeleted(name, before); + } +} 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 new file mode 100644 index 0000000..c91daa4 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.plugins.index.property; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider; +import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +@Component +@Service(IndexEditorProvider.class) +public class OrderedPropertyIndexEditorProvider implements IndexEditorProvider, OrderedIndex { + + @Override + @CheckForNull + public Editor getIndexEditor(@Nonnull String type, @Nonnull NodeBuilder definition, @Nonnull NodeState root, @Nonnull IndexUpdateCallback callback) throws CommitFailedException { + Editor editor = (TYPE.equals(type)) ? new OrderedPropertyIndexEditor(definition,root,callback) : null; + return editor; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java new file mode 100644 index 0000000..9a84e4f --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java @@ -0,0 +1,44 @@ +/* + * 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 org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy; +import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * + */ +public class OrderedPropertyIndexLookup extends PropertyIndexLookup { + + private static final IndexStoreStrategy STORE = new OrderedContentMirrorStoreStrategy(); + + public OrderedPropertyIndexLookup(NodeState root) { + super(root); + } + + @Override + IndexStoreStrategy getStrategy(NodeState indexMeta) { + return STORE; + } + + @Override + String getType() { + return OrderedIndex.TYPE; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexProvider.java new file mode 100644 index 0000000..ab6d137 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexProvider.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.plugins.index.property; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.ImmutableList; + +@Component +@Service(QueryIndexProvider.class) +public class OrderedPropertyIndexProvider implements QueryIndexProvider { + + @Override + @Nonnull + public List getQueryIndexes(NodeState nodeState) { + return ImmutableList. of(new OrderedPropertyIndex()); + } +} 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 e7c19c0..4b8dd9d 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 @@ -120,6 +120,16 @@ class PropertyIndex implements QueryIndex { return "property"; } + /** + * return the proper implementation of the Lookup + * + * @param root + * @return the lookup + */ + PropertyIndexLookup getLookup(NodeState root) { + return new PropertyIndexLookup(root); + } + @Override public double getCost(Filter filter, NodeState root) { if (filter.getFullTextConstraint() != null) { @@ -127,7 +137,7 @@ class PropertyIndex implements QueryIndex { return Double.POSITIVE_INFINITY; } - PropertyIndexLookup lookup = new PropertyIndexLookup(root); + PropertyIndexLookup lookup = getLookup(root); for (PropertyRestriction pr : filter.getPropertyRestrictions()) { String propertyName = PathUtils.getName(pr.propertyName); // TODO support indexes on a path @@ -157,7 +167,7 @@ class PropertyIndex implements QueryIndex { public Cursor query(Filter filter, NodeState root) { Iterable paths = null; - PropertyIndexLookup lookup = new PropertyIndexLookup(root); + PropertyIndexLookup lookup = getLookup(root); int depth = 1; for (PropertyRestriction pr : filter.getPropertyRestrictions()) { String propertyName = PathUtils.getName(pr.propertyName); @@ -202,7 +212,7 @@ class PropertyIndex implements QueryIndex { public String getPlan(Filter filter, NodeState root) { StringBuilder buff = new StringBuilder("property"); StringBuilder notIndexed = new StringBuilder(); - PropertyIndexLookup lookup = new PropertyIndexLookup(root); + PropertyIndexLookup lookup = getLookup(root); for (PropertyRestriction pr : filter.getPropertyRestrictions()) { String propertyName = PathUtils.getName(pr.propertyName); // TODO support indexes on a path diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexEditor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexEditor.java index ddfbe13..3036d82 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexEditor.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexEditor.java @@ -31,6 +31,7 @@ import static org.apache.jackrabbit.oak.plugins.index.property.PropertyIndex.enc import java.util.Set; +import javax.annotation.Nonnull; import javax.jcr.PropertyType; import org.apache.jackrabbit.oak.api.CommitFailedException; @@ -111,6 +112,8 @@ class PropertyIndexEditor implements IndexEditor { this.path = "/"; this.definition = definition; + //initPropertyNames(definition); + // get property names PropertyState names = definition.getProperty(PROPERTY_NAMES); if (names.count() == 1) { @@ -138,18 +141,27 @@ class PropertyIndexEditor implements IndexEditor { this.updateCallback = updateCallback; } - private PropertyIndexEditor(PropertyIndexEditor parent, String name) { + PropertyIndexEditor(PropertyIndexEditor parent, String name) { this.parent = parent; this.name = name; this.path = null; this.definition = parent.definition; - this.propertyNames = parent.propertyNames; + this.propertyNames = parent.getPropertyNames(); this.typePredicate = parent.typePredicate; this.keysToCheckForUniqueness = parent.keysToCheckForUniqueness; this.updateCallback = parent.updateCallback; } /** + * commodity method for allowing extensions + * + * @return the propertyNames + */ + Set getPropertyNames() { + return propertyNames; + } + + /** * Returns the path of this node, building it lazily when first requested. */ private String getPath() { @@ -193,7 +205,7 @@ class PropertyIndexEditor implements IndexEditor { return keys; } - private static IndexStoreStrategy getStrategy(boolean unique) { + IndexStoreStrategy getStrategy(boolean unique) { return unique ? UNIQUE : MIRROR; } @@ -214,8 +226,8 @@ class PropertyIndexEditor implements IndexEditor { if (typeChanged) { // possible type change, so ignore diff results and // just load all matching values from both states - beforeKeys = getMatchingKeys(before, propertyNames); - afterKeys = getMatchingKeys(after, propertyNames); + beforeKeys = getMatchingKeys(before, getPropertyNames()); + afterKeys = getMatchingKeys(after, getPropertyNames()); } if (beforeKeys != null && !typePredicate.apply(before)) { // the before state doesn't match the type, so clear its values @@ -282,7 +294,7 @@ class PropertyIndexEditor implements IndexEditor { public void propertyAdded(PropertyState after) { String name = after.getName(); typeChanged = typeChanged || isTypeProperty(name); - if (propertyNames.contains(name)) { + if (getPropertyNames().contains(name)) { afterKeys = addValueKeys(afterKeys, after); } } @@ -291,7 +303,7 @@ class PropertyIndexEditor implements IndexEditor { public void propertyChanged(PropertyState before, PropertyState after) { String name = after.getName(); typeChanged = typeChanged || isTypeProperty(name); - if (propertyNames.contains(name)) { + if (getPropertyNames().contains(name)) { beforeKeys = addValueKeys(beforeKeys, before); afterKeys = addValueKeys(afterKeys, after); } @@ -301,25 +313,36 @@ class PropertyIndexEditor implements IndexEditor { public void propertyDeleted(PropertyState before) { String name = before.getName(); typeChanged = typeChanged || isTypeProperty(name); - if (propertyNames.contains(name)) { + if (getPropertyNames().contains(name)) { beforeKeys = addValueKeys(beforeKeys, before); } } + /** + * Retrieve a new index editor associated with the child node to process + * + * @param parent the index editor related to the parent node + * @param name the name of the child node + * @return an instance of the PropertyIndexEditor + */ + PropertyIndexEditor getChildIndexEditor(@Nonnull PropertyIndexEditor parent, @Nonnull String name){ + return new PropertyIndexEditor(parent, name); + } + @Override public Editor childNodeAdded(String name, NodeState after) { - return new PropertyIndexEditor(this, name); + return getChildIndexEditor(this, name); } @Override public Editor childNodeChanged( String name, NodeState before, NodeState after) { - return new PropertyIndexEditor(this, name); + return getChildIndexEditor(this, name); } @Override public Editor childNodeDeleted(String name, NodeState before) { - return new PropertyIndexEditor(this, name); + return getChildIndexEditor(this, name); } } diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java index 271e101..08a31fd 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java @@ -25,7 +25,6 @@ import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTE import static org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider.TYPE; import static org.apache.jackrabbit.oak.plugins.index.property.PropertyIndex.encode; -import java.util.Iterator; import java.util.Set; import javax.annotation.Nullable; @@ -100,12 +99,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; } @@ -118,7 +116,7 @@ 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; } @@ -146,8 +144,7 @@ public class PropertyIndexLookup { * 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 +153,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)) { @@ -185,6 +182,15 @@ public class PropertyIndexLookup { return fallback; } + /** + * retrieve the type of the index + * + * @return the type + */ + 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..c759eed 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; @@ -79,7 +81,7 @@ public class ContentMirrorStoreStrategy implements IndexStoreStrategy { } } - private static void remove(NodeBuilder index, String key, String value) { + private void remove(NodeBuilder index, String key, String value) { NodeBuilder builder = index.getChildNode(key); if (builder.exists()) { // Collect all builders along the given path @@ -98,18 +100,13 @@ public class ContentMirrorStoreStrategy implements IndexStoreStrategy { } // 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(); - } - } + prune(index, builders); } } - private static void insert(NodeBuilder index, String key, String value) { - NodeBuilder builder = index.child(key); + 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); } @@ -126,7 +123,7 @@ public class ContentMirrorStoreStrategy implements IndexStoreStrategy { PathIterator it = new PathIterator(filter, indexName); if (values == null) { it.setPathContainsValue(true); - it.enqueue(index.getChildNodeEntries().iterator()); + it.enqueue(getChildNodeEntries(index).iterator()); } else { for (String p : values) { NodeState property = index.getChildNode(p); @@ -142,6 +139,12 @@ public class ContentMirrorStoreStrategy implements IndexStoreStrategy { }; } + @Nonnull + Iterable getChildNodeEntries(@Nonnull + final NodeState index) { + return index.getChildNodeEntries(); + } + @Override public Iterable query(final Filter filter, final String indexName, final NodeState indexMeta, final Iterable values) { @@ -405,4 +408,35 @@ public class ContentMirrorStoreStrategy implements IndexStoreStrategy { } + /** + * 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); + } + + /** + * 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(); + } + } + } } 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..2c76e4e --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java @@ -0,0 +1,273 @@ +/* + * 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; + +/** + * Same as for {@link ContentMirrorStoreStrategy} but the order of the keys is kept by using the + * following structure + * + * + * :index : { + * :start : { :next = n1 }, + * n0 : { /content/foo/bar(match=true), :next=n3 }, + * n1 : { /content/foo1/bar(match=true), :next=n0 }, + * n2 : { /content/foo2/bar(match=true), :next= }, //this is the end of the list + * n3 : { /content/foo3/bar(match=true), :next=n2 } + * } + * + */ +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 localkey = null; + NodeBuilder start = index.child(START); + + // identifying the right place for insert + String n = start.getString(NEXT); + if (Strings.isNullOrEmpty(n)) { + // new/empty index + localkey = index.child(key); + localkey.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) { + localkey = index.child(key); + localkey.setProperty(NEXT, nextKey); + start.setProperty(NEXT, key); + } else { + Iterable children = getChildNodeEntries(index.getNodeState()); + for (ChildNodeEntry child : children) { + nextKey = child.getNodeState().getString(NEXT); + if (Strings.isNullOrEmpty(nextKey)) { + // we're at the last element, therefore our 'key' has to be appended + index.getChildNode(child.getName()).setProperty(NEXT, key); + localkey = index.child(key); + localkey.setProperty(NEXT, ""); + } else { + if (key.compareTo(nextKey) < 0) { + index.getChildNode(child.getName()).setProperty(NEXT, key); + localkey = index.child(key); + localkey.setProperty(NEXT, nextKey); + break; + } + } + } + } + } + + return localkey; + } + + @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 = ""; + } + // (3) re-link the previous to the next + index.getChildNode(previous.getName()).setProperty(NEXT, next); + } + node.remove(); + } + } + } + + @Nullable + ChildNodeEntry findPrevious(@Nonnull final NodeState index, @Nonnull final NodeState node) { + ChildNodeEntry previous = null; + ChildNodeEntry current = null; + boolean found = false; + Iterator 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 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 getChildNodeEntries(@Nonnull final NodeState index, + final boolean includeStart) { + Iterable 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 localIndex = index; + private NodeState localStart = ((includeStart && !start.exists()) ? EMPTY_START_NODE + : start); + private NodeState current = localStart; + private boolean localIncludeStart = includeStart; + + @Override + public Iterator iterator() { + return new Iterator() { + + @Override + public boolean hasNext() { + return ((localIncludeStart && localStart.equals(current)) || (!localIncludeStart && !Strings.isNullOrEmpty(current.getString(NEXT)))); + } + + @Override + public ChildNodeEntry next() { + ChildNodeEntry localCNE = null; + if (localIncludeStart && localStart.equals(current)) { + localCNE = new OrderedChildNodeEntry(START, current); + // let's set it to false. We just included it. + localIncludeStart = false; + } else { + if (hasNext()) { + final String name = current.getString(NEXT); + current = localIndex.getChildNode(name); + localCNE = new OrderedChildNodeEntry(name, current); + } else { + throw new NoSuchElementException(); + } + } + return localCNE; + } + + @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; + } + + @Override + @Nonnull + public String getName() { + return name; + } + + @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..f75fbdb --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java @@ -0,0 +1,84 @@ +/* + * 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; +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 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 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 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..fb4a51d --- /dev/null +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java @@ -0,0 +1,975 @@ +/* + * 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())); + + // 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())); + + // 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())); + + // 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 children = store.getChildNodeEntries(index.getNodeState(), true); + assertEquals("Wrong size of Iterable", 1, Iterators.size(children.iterator())); + + Iterator 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 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 cd8c529..116e596 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