You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2022/07/04 16:14:57 UTC
[jackrabbit-oak] branch OAK-9780 updated: OAK-9780 Prefetch Cursor
This is an automated email from the ASF dual-hosted git repository.
thomasm pushed a commit to branch OAK-9780
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/OAK-9780 by this push:
new 5f8a17634d OAK-9780 Prefetch Cursor
5f8a17634d is described below
commit 5f8a17634ddc488590d8d59fdf9873d9a5ce25ce
Author: Thomas Mueller <th...@apache.org>
AuthorDate: Mon Jul 4 18:14:47 2022 +0200
OAK-9780 Prefetch Cursor
---
.../jackrabbit/oak/plugins/index/Cursors.java | 624 ---------------------
.../plugins/index/aggregate/AggregateIndex.java | 2 +-
.../plugins/index/aggregate/AggregationCursor.java | 2 +-
.../oak/plugins/index/cursor/AbstractCursor.java | 38 ++
.../oak/plugins/index/cursor/AncestorCursor.java | 62 ++
.../oak/plugins/index/cursor/ConcatCursor.java | 120 ++++
.../oak/plugins/index/cursor/Cursors.java | 113 ++++
.../plugins/index/cursor/IntersectionCursor.java | 121 ++++
.../oak/plugins/index/cursor/PathCursor.java | 73 +++
.../oak/plugins/index/cursor/PrefetchCursor.java | 203 +++++++
.../oak/plugins/index/cursor/TraversingCursor.java | 192 +++++++
.../oak/plugins/index/diffindex/DiffIndex.java | 2 +-
.../oak/plugins/index/nodetype/NodeTypeIndex.java | 3 +-
.../plugins/index/property/PropertyIndexPlan.java | 2 +-
.../plugins/index/reference/ReferenceIndex.java | 2 +-
.../jackrabbit/oak/query/ast/SelectorImpl.java | 2 +-
.../oak/query/index/TraversingIndex.java | 2 +-
.../oak/plugins/index/cursor/CursorUtils.java | 34 ++
.../plugins/index/{ => cursor}/CursorsTest.java | 2 +-
.../plugins/index/cursor/PrefetchCursorTest.java | 119 ++++
.../oak/plugins/index/cursor/TestCursor.java | 41 ++
.../index/cursor/TestPrefetchNodeStore.java | 42 ++
.../oak/plugins/index/cursor/TestRow.java | 45 ++
.../plugins/index/nodetype/NodeTypeIndexTest.java | 4 +-
.../oak/security/CustomQueryIndexProviderTest.java | 2 +-
25 files changed, 1215 insertions(+), 637 deletions(-)
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java
deleted file mode 100644
index e5125febd6..0000000000
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-
-import org.apache.jackrabbit.oak.api.Result.SizePrecision;
-import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
-import org.apache.jackrabbit.oak.query.FilterIterators;
-import org.apache.jackrabbit.oak.query.QueryImpl;
-import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
-import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.spi.query.Filter;
-import org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction;
-import org.apache.jackrabbit.oak.spi.query.IndexRow;
-import org.apache.jackrabbit.oak.spi.query.QueryLimits;
-import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
-import org.apache.jackrabbit.oak.spi.state.NodeState;
-import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
-import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
-import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Queues;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
-import static org.apache.jackrabbit.oak.spi.query.QueryConstants.REP_FACET;
-
-/**
- * This utility class provides factory methods to create commonly used types of
- * {@link Cursor}s.
- */
-public class Cursors {
-
- private Cursors() {
- }
-
- public static void checkMemoryLimit(long count, QueryLimits settings) {
- FilterIterators.checkMemoryLimit(count, settings);
- }
-
- public static void checkReadLimit(long count, QueryLimits settings) {
- FilterIterators.checkReadLimit(count, settings);
- }
-
- public static Cursor newIntersectionCursor(Cursor a, Cursor b, QueryLimits settings) {
- return new IntersectionCursor(a, b, settings);
- }
-
- public static Cursor newConcatCursor(List<Cursor> cursors, QueryLimits settings) {
- return new ConcatCursor(cursors, settings);
- }
-
- public static Cursor newPrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount,
- NodeState rootState, List<String> prefetchRelative) {
- return new PrefetchCursor(cursor, store, prefetchCount, rootState, prefetchRelative);
- }
-
- /**
- * Creates a {@link Cursor} over paths.
- *
- * @param paths the paths to iterate over (must return distinct paths)
- * @return the Cursor.
- */
- public static Cursor newPathCursor(Iterable<String> paths, QueryLimits settings) {
- return new PathCursor(paths.iterator(), true, settings);
- }
-
- /**
- * Creates a {@link Cursor} over paths, and make the result distinct.
- * The iterator might return duplicate paths
- *
- * @param paths the paths to iterate over (might contain duplicate entries)
- * @return the Cursor.
- */
- public static Cursor newPathCursorDistinct(Iterable<String> paths, QueryLimits settings) {
- return new PathCursor(paths.iterator(), true, settings);
- }
-
- /**
- * Returns a traversing cursor based on the path restriction in the given
- * {@link Filter}.
- *
- * @param filter the filter.
- * @param rootState the root {@link NodeState}.
- * @return the {@link Cursor}.
- */
- public static Cursor newTraversingCursor(Filter filter,
- NodeState rootState) {
- return new TraversingCursor(filter, rootState);
- }
-
- /**
- * Returns a cursor wrapper, which returns the ancestor rows at the given
- * <code>level</code> of the wrapped cursor <code>c</code>. With
- * <code>level</code> e.g. set to <code>1</code>, the returned cursor
- * iterates over the parent rows of the passed cursor <code>c</code>. The
- * returned cursor guarantees distinct rows.
- *
- * @param c the cursor to wrap.
- * @param level the ancestor level. Must be {@code >= 1}.
- * @return cursor over the ancestors of <code>c</code> at <code>level</code>.
- */
- public static Cursor newAncestorCursor(Cursor c, int level, QueryLimits settings) {
- checkNotNull(c);
- checkArgument(level >= 1);
- return new AncestorCursor(c, level, settings);
- }
-
- /**
- * A Cursor implementation where the remove method throws an
- * UnsupportedOperationException.
- */
- public abstract static class AbstractCursor implements Cursor {
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public long getSize(SizePrecision precision, long max) {
- return -1;
- }
-
- }
-
- /**
- * This class allows to iterate over the parent nodes of the wrapped cursor.
- */
- private static class AncestorCursor extends PathCursor {
-
- public AncestorCursor(Cursor cursor, int level, QueryLimits settings) {
- super(transform(cursor, level), true, settings);
- }
-
- private static Iterator<String> transform(Cursor cursor, final int level) {
- Iterator<String> unfiltered = Iterators.transform(cursor,
- new Function<IndexRow, String>() {
- @Override
- public String apply(@Nullable IndexRow input) {
- return input != null ? input.getPath() : null;
- }
- });
- Iterator<String> filtered = Iterators.filter(unfiltered,
- new Predicate<String>() {
- @Override
- public boolean apply(@Nullable String input) {
- return input != null && PathUtils.getDepth(input) >= level;
- }
- });
- return Iterators.transform(filtered, new Function<String, String>() {
- @Override
- public String apply(String input) {
- return PathUtils.getAncestorPath(input, level);
- }
- });
- }
- }
-
- /**
- * <code>PathCursor</code> implements a simple {@link Cursor} that iterates
- * over a {@link String} based path {@link Iterable}.
- */
- public static class PathCursor extends AbstractCursor {
-
- private final Iterator<String> iterator;
-
- public PathCursor(Iterator<String> paths, boolean distinct, final QueryLimits settings) {
- Iterator<String> it = paths;
- if (distinct) {
- it = Iterators.filter(it, new Predicate<String>() {
-
- private final HashSet<String> known = new HashSet<String>();
-
- @Override
- public boolean apply(@Nullable String input) {
- FilterIterators.checkMemoryLimit(known.size(), settings);
- // Set.add returns true for new entries
- return known.add(input);
- }
-
- });
- }
- this.iterator = it;
- }
-
- @Override
- public IndexRow next() {
- // TODO support jcr:score and possibly rep:excerpt
- String path = iterator.next();
- return new IndexRowImpl(isAbsolute(path) ? path : "/" + path);
- }
-
- @Override
- public boolean hasNext() {
- return iterator.hasNext();
- }
-
- }
-
- /**
- * A cursor that reads all nodes in a given subtree.
- */
- private static class TraversingCursor extends AbstractCursor {
-
- private static final Logger LOG = LoggerFactory.getLogger(TraversingCursor.class);
-
- private final Filter filter;
-
- private final Deque<Iterator<? extends ChildNodeEntry>> nodeIterators =
- Queues.newArrayDeque();
-
- private String parentPath;
-
- private String currentPath;
-
- private long readCount;
-
- private boolean init;
-
- private boolean closed;
-
- private final QueryLimits settings;
-
- public TraversingCursor(Filter filter, NodeState rootState) {
- this.filter = filter;
- this.settings = filter.getQueryLimits();
-
- String path = filter.getPath();
- parentPath = null;
- currentPath = "/";
- NodeState parent = null;
- NodeState node = rootState;
-
- if (filter.containsNativeConstraint()) {
- // OAK-4313: if no other index was found,
- // then, for native queries, we won't match anything
- return;
- }
-
- if (filter.isAlwaysFalse()) {
- // nothing can match this filter, leave nodes empty
- return;
- }
-
- Filter.PropertyRestriction facetRestriction = filter.getPropertyRestriction(REP_FACET);
- if (facetRestriction != null) {
- // we don't evaluate facets by traversal
- throw new IllegalArgumentException(facetRestriction + " can't be evaluated by traversal");
- }
-
- if (!path.equals("/")) {
- for (String name : path.substring(1).split("/")) {
- parentPath = currentPath;
- currentPath = PathUtils.concat(parentPath, name);
-
- parent = node;
- node = parent.getChildNode(name);
- }
- if (!node.exists()) {
- // nothing can match this filter, leave nodes empty
- return;
- }
- }
- Filter.PathRestriction restriction = filter.getPathRestriction();
- switch (restriction) {
- case NO_RESTRICTION:
- case EXACT:
- case ALL_CHILDREN:
- nodeIterators.add(Iterators.singletonIterator(
- new MemoryChildNodeEntry(currentPath, node)));
- parentPath = "";
- break;
- case PARENT:
- if (parent != null) {
- nodeIterators.add(Iterators.singletonIterator(
- new MemoryChildNodeEntry(parentPath, parent)));
- parentPath = "";
- }
- break;
- case DIRECT_CHILDREN:
- nodeIterators.add(node.getChildNodeEntries().iterator());
- parentPath = currentPath;
- break;
- default:
- throw new IllegalArgumentException("Unknown restriction: " + restriction);
- }
- }
-
- @Override
- public IndexRow next() {
- if (closed) {
- throw new IllegalStateException("This cursor is closed");
- }
- if (!init) {
- fetchNext();
- init = true;
- }
- IndexRowImpl result = new IndexRowImpl(currentPath);
- fetchNext();
- return result;
- }
-
- @Override
- public boolean hasNext() {
- if (!closed && !init) {
- fetchNext();
- init = true;
- }
- return !closed;
- }
-
- private void fetchNext() {
- while (!nodeIterators.isEmpty()) {
- Iterator<? extends ChildNodeEntry> iterator = nodeIterators.getLast();
- if (iterator.hasNext()) {
- ChildNodeEntry entry = iterator.next();
-
- readCount++;
- if (readCount % 1000 == 0) {
- FilterIterators.checkReadLimit(readCount, settings);
- String caller = IndexUtils.getCaller(this.settings.getIgnoredClassNamesInCallTrace());
- LOG.warn("Traversed {} nodes with filter {} called by {}; consider creating an index or changing the query" ,
- readCount, filter, caller);
- }
-
- NodeState node = entry.getNodeState();
-
- String name = entry.getName();
- if (NodeStateUtils.isHidden(name)) {
- continue;
- }
- currentPath = PathUtils.concat(parentPath, name);
-
- PathRestriction r = filter.getPathRestriction();
- if (r == PathRestriction.ALL_CHILDREN ||
- r == PathRestriction.NO_RESTRICTION) {
- nodeIterators.addLast(node.getChildNodeEntries().iterator());
- parentPath = currentPath;
- }
- return;
- } else {
- nodeIterators.removeLast();
- parentPath = PathUtils.getParentPath(parentPath);
- }
- }
- currentPath = null;
- closed = true;
- }
-
- }
-
- /**
- * A cursor that intersects two cursors.
- */
- private static class IntersectionCursor extends AbstractCursor {
-
- private final HashMap<String, IndexRow> secondSet = new HashMap<String, IndexRow>();
- private final HashSet<String> seen = new HashSet<String>();
- private final Cursor first, second;
- private final QueryLimits settings;
- private boolean init;
- private boolean closed;
- private IndexRow current;
-
- IntersectionCursor(Cursor first, Cursor second, QueryLimits settings) {
- this.first = first;
- this.second = second;
- this.settings = settings;
- }
-
- @Override
- public IndexRow next() {
- if (closed) {
- throw new IllegalStateException("This cursor is closed");
- }
- if (!init) {
- fetchNext();
- init = true;
- if (closed) {
- throw new IllegalStateException("This cursor is closed");
- }
- }
- IndexRow result = current;
- // fetchNext();
- init = false;
- return result;
- }
-
- @Override
- public boolean hasNext() {
- if (!closed && !init) {
- fetchNext();
- init = true;
- }
- return !closed;
- }
-
- private void fetchNext() {
- while (true) {
- if (!first.hasNext()) {
- closed = true;
- return;
- }
- IndexRow c = first.next();
- String p = c.getPath();
- if (seen.contains(p)) {
- continue;
- }
- if (secondSet.remove(p) != null) {
- current = c;
- markSeen(p);
- return;
- }
- while (second.hasNext()) {
- IndexRow s = second.next();
- String p2 = s.getPath();
- if (p.equals(p2)) {
- current = c;
- markSeen(p);
- return;
- }
- secondSet.put(p2, s);
- FilterIterators.checkMemoryLimit(secondSet.size(), settings);
- }
- }
- }
-
- private void markSeen(String path) {
- seen.add(path);
- FilterIterators.checkMemoryLimit(seen.size(), settings);
- }
-
- @Override
- public long getSize(SizePrecision precision, long max) {
- // this is the worst case
- long a = first.getSize(precision, max);
- long b = second.getSize(precision, max);
- if (a < 0 || b < 0) {
- return -1;
- }
- return QueryImpl.saturatedAdd(a, b);
- }
-
- }
-
- /**
- * A cursor that combines multiple cursors into a single cursor.
- */
- private static class ConcatCursor extends AbstractCursor {
-
- private final HashSet<String> seen = new HashSet<String>();
- private final List<Cursor> cursors;
- private final QueryLimits settings;
- private boolean init;
- private boolean closed;
-
- private Cursor currentCursor;
- private int cursorListIndex;
- private IndexRow current;
-
- ConcatCursor(List<Cursor> cursors, QueryLimits settings) {
- this.cursors = cursors;
- this.settings = settings;
- nextCursor();
- }
-
- private void nextCursor() {
- if (cursorListIndex >= cursors.size()) {
- init = true;
- closed = true;
- } else {
- currentCursor = cursors.get(cursorListIndex++);
- }
- }
-
- @Override
- public IndexRow next() {
- if (closed) {
- throw new IllegalStateException("This cursor is closed");
- }
- if (!init) {
- fetchNext();
- init = true;
- }
- IndexRow result = current;
- init = false;
- return result;
- }
-
- @Override
- public boolean hasNext() {
- if (!closed && !init) {
- fetchNext();
- init = true;
- }
- return !closed;
- }
-
- private void fetchNext() {
- while (true) {
- while (!currentCursor.hasNext()) {
- nextCursor();
- if (closed) {
- return;
- }
- }
- IndexRow c = currentCursor.next();
- String p = c.getPath();
- if (seen.contains(p)) {
- continue;
- }
- current = c;
- markSeen(p);
- return;
- }
- }
-
- private void markSeen(String path) {
- seen.add(path);
- FilterIterators.checkMemoryLimit(seen.size(), settings);
- }
-
- @Override
- public long getSize(SizePrecision precision, long max) {
- // this is the worst case (duplicate entries are counted twice)
- long total = 0;
- for (Cursor c : cursors) {
- long t = c.getSize(precision, max);
- if (t < 0) {
- return -1;
- }
- total = QueryImpl.saturatedAdd(total, t);
- }
- return total;
- }
-
- }
-
- /**
- * A cursor that prefetches nodes from the node store
- */
- private static class PrefetchCursor extends AbstractCursor {
-
- private final Cursor cursor;
- private final PrefetchNodeStore store;
- private final int prefetchCount;
- private final NodeState rootState;
- private Iterator<IndexRow> prefetched;
- private final List<String> prefetchRelative;
-
- PrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount, NodeState rootState, List<String> prefetchRelative) {
- this.cursor = cursor;
- this.store = store;
- this.prefetchCount = prefetchCount;
- this.rootState = rootState;
- this.prefetched = Iterators.emptyIterator();
- this.prefetchRelative = prefetchRelative;
- }
-
- @Override
- public IndexRow next() {
- if (!prefetched.hasNext()) {
- ArrayList<IndexRow> rows = new ArrayList<>();
- TreeSet<String> paths = new TreeSet<>();
- for (int i = 0; i < prefetchCount && cursor.hasNext(); i++) {
- IndexRow row = cursor.next();
- rows.add(row);
- String p = row.getPath();
- prefetchRelative(paths, p);
- do {
- paths.add(p);
- p = PathUtils.getParentPath(p);
- } while (!PathUtils.denotesRoot(p));
- }
- store.prefetch(paths, rootState);
- prefetched = rows.iterator();
- }
- return prefetched.next();
- }
-
- private void prefetchRelative(Set<String> target, String p) {
- for (String r : prefetchRelative) {
- String concat = PathUtils.concat(p, r);
- target.add(concat);
- }
- }
-
- @Override
- public boolean hasNext() {
- return prefetched.hasNext() || cursor.hasNext();
- }
- }
-
-}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
index d14a7b74be..a725cd68a4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
@@ -27,9 +27,9 @@ import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextOr;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextTerm;
import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextVisitor;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
index 1862ed9cb0..87065a9d64 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
@@ -23,9 +23,9 @@ import java.util.Set;
import org.apache.jackrabbit.oak.api.PropertyValue;
import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.plugins.index.cursor.AbstractCursor;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.spi.query.IndexRow;
-import org.apache.jackrabbit.oak.plugins.index.Cursors.AbstractCursor;
import org.apache.jackrabbit.oak.spi.query.QueryIndex;
import org.apache.jackrabbit.oak.spi.state.NodeState;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java
new file mode 100644
index 0000000000..ed31f9de2c
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java
@@ -0,0 +1,38 @@
+/*
+ * 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.cursor;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+
+/**
+ * A Cursor implementation where the remove method throws an
+ * UnsupportedOperationException.
+ */
+public abstract class AbstractCursor implements Cursor {
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getSize(SizePrecision precision, long max) {
+ return -1;
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java
new file mode 100644
index 0000000000..1cee1ca256
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java
@@ -0,0 +1,62 @@
+/*
+ * 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.cursor;
+
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterators;
+
+/**
+ * This class allows to iterate over the parent nodes of the wrapped cursor.
+ */
+class AncestorCursor extends PathCursor {
+
+ public AncestorCursor(Cursor cursor, int level, QueryLimits settings) {
+ super(transform(cursor, level), true, settings);
+ }
+
+ private static Iterator<String> transform(Cursor cursor, final int level) {
+ Iterator<String> unfiltered = Iterators.transform(cursor,
+ new Function<IndexRow, String>() {
+ @Override
+ public String apply(@Nullable IndexRow input) {
+ return input != null ? input.getPath() : null;
+ }
+ });
+ Iterator<String> filtered = Iterators.filter(unfiltered,
+ new Predicate<String>() {
+ @Override
+ public boolean apply(@Nullable String input) {
+ return input != null && PathUtils.getDepth(input) >= level;
+ }
+ });
+ return Iterators.transform(filtered, new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ return PathUtils.getAncestorPath(input, level);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java
new file mode 100644
index 0000000000..c20494fde8
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java
@@ -0,0 +1,120 @@
+/*
+ * 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.cursor;
+
+import java.util.HashSet;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.QueryImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+
+/**
+ * A cursor that combines multiple cursors into a single cursor.
+ */
+class ConcatCursor extends AbstractCursor {
+
+ private final HashSet<String> seen = new HashSet<String>();
+ private final List<Cursor> cursors;
+ private final QueryLimits settings;
+ private boolean init;
+ private boolean closed;
+
+ private Cursor currentCursor;
+ private int cursorListIndex;
+ private IndexRow current;
+
+ ConcatCursor(List<Cursor> cursors, QueryLimits settings) {
+ this.cursors = cursors;
+ this.settings = settings;
+ nextCursor();
+ }
+
+ private void nextCursor() {
+ if (cursorListIndex >= cursors.size()) {
+ init = true;
+ closed = true;
+ } else {
+ currentCursor = cursors.get(cursorListIndex++);
+ }
+ }
+
+ @Override
+ public IndexRow next() {
+ if (closed) {
+ throw new IllegalStateException("This cursor is closed");
+ }
+ if (!init) {
+ fetchNext();
+ init = true;
+ }
+ IndexRow result = current;
+ init = false;
+ return result;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!closed && !init) {
+ fetchNext();
+ init = true;
+ }
+ return !closed;
+ }
+
+ private void fetchNext() {
+ while (true) {
+ while (!currentCursor.hasNext()) {
+ nextCursor();
+ if (closed) {
+ return;
+ }
+ }
+ IndexRow c = currentCursor.next();
+ String p = c.getPath();
+ if (seen.contains(p)) {
+ continue;
+ }
+ current = c;
+ markSeen(p);
+ return;
+ }
+ }
+
+ private void markSeen(String path) {
+ seen.add(path);
+ FilterIterators.checkMemoryLimit(seen.size(), settings);
+ }
+
+ @Override
+ public long getSize(SizePrecision precision, long max) {
+ // this is the worst case (duplicate entries are counted twice)
+ long total = 0;
+ for (Cursor c : cursors) {
+ long t = c.getSize(precision, max);
+ if (t < 0) {
+ return -1;
+ }
+ total = QueryImpl.saturatedAdd(total, t);
+ }
+ return total;
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java
new file mode 100644
index 0000000000..62ad15a3fc
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java
@@ -0,0 +1,113 @@
+/*
+ * 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.cursor;
+
+import java.util.List;
+
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * This utility class provides factory methods to create commonly used types of
+ * {@link Cursor}s.
+ */
+public class Cursors {
+
+ private Cursors() {
+ }
+
+ public static void checkMemoryLimit(long count, QueryLimits settings) {
+ FilterIterators.checkMemoryLimit(count, settings);
+ }
+
+ public static void checkReadLimit(long count, QueryLimits settings) {
+ FilterIterators.checkReadLimit(count, settings);
+ }
+
+ public static Cursor newIntersectionCursor(Cursor a, Cursor b, QueryLimits settings) {
+ return new IntersectionCursor(a, b, settings);
+ }
+
+ public static Cursor newConcatCursor(List<Cursor> cursors, QueryLimits settings) {
+ return new ConcatCursor(cursors, settings);
+ }
+
+ public static Cursor newPrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount,
+ NodeState rootState, List<String> prefetchRelative) {
+ return new PrefetchCursor(cursor, store, prefetchCount, rootState, prefetchRelative);
+ }
+
+ /**
+ * Creates a {@link Cursor} over paths.
+ *
+ * @param paths the paths to iterate over (must return distinct paths)
+ * @return the Cursor.
+ */
+ public static Cursor newPathCursor(Iterable<String> paths, QueryLimits settings) {
+ return new PathCursor(paths.iterator(), true, settings);
+ }
+
+ /**
+ * Creates a {@link Cursor} over paths, and make the result distinct.
+ * The iterator might return duplicate paths
+ *
+ * @param paths the paths to iterate over (might contain duplicate entries)
+ * @return the Cursor.
+ */
+ public static Cursor newPathCursorDistinct(Iterable<String> paths, QueryLimits settings) {
+ return new PathCursor(paths.iterator(), true, settings);
+ }
+
+ /**
+ * Returns a traversing cursor based on the path restriction in the given
+ * {@link Filter}.
+ *
+ * @param filter the filter.
+ * @param rootState the root {@link NodeState}.
+ * @return the {@link Cursor}.
+ */
+ public static Cursor newTraversingCursor(Filter filter,
+ NodeState rootState) {
+ return new TraversingCursor(filter, rootState);
+ }
+
+ /**
+ * Returns a cursor wrapper, which returns the ancestor rows at the given
+ * <code>level</code> of the wrapped cursor <code>c</code>. With
+ * <code>level</code> e.g. set to <code>1</code>, the returned cursor
+ * iterates over the parent rows of the passed cursor <code>c</code>. The
+ * returned cursor guarantees distinct rows.
+ *
+ * @param c the cursor to wrap.
+ * @param level the ancestor level. Must be {@code >= 1}.
+ * @return cursor over the ancestors of <code>c</code> at <code>level</code>.
+ */
+ public static Cursor newAncestorCursor(Cursor c, int level, QueryLimits settings) {
+ checkNotNull(c);
+ checkArgument(level >= 1);
+ return new AncestorCursor(c, level, settings);
+ }
+
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java
new file mode 100644
index 0000000000..41a2b373e5
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java
@@ -0,0 +1,121 @@
+/*
+ * 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.cursor;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.QueryImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+
+/**
+ * A cursor that intersects two cursors.
+ */
+class IntersectionCursor extends AbstractCursor {
+
+ private final HashMap<String, IndexRow> secondSet = new HashMap<String, IndexRow>();
+ private final HashSet<String> seen = new HashSet<String>();
+ private final Cursor first, second;
+ private final QueryLimits settings;
+ private boolean init;
+ private boolean closed;
+ private IndexRow current;
+
+ IntersectionCursor(Cursor first, Cursor second, QueryLimits settings) {
+ this.first = first;
+ this.second = second;
+ this.settings = settings;
+ }
+
+ @Override
+ public IndexRow next() {
+ if (closed) {
+ throw new IllegalStateException("This cursor is closed");
+ }
+ if (!init) {
+ fetchNext();
+ init = true;
+ if (closed) {
+ throw new IllegalStateException("This cursor is closed");
+ }
+ }
+ IndexRow result = current;
+ // fetchNext();
+ init = false;
+ return result;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!closed && !init) {
+ fetchNext();
+ init = true;
+ }
+ return !closed;
+ }
+
+ private void fetchNext() {
+ while (true) {
+ if (!first.hasNext()) {
+ closed = true;
+ return;
+ }
+ IndexRow c = first.next();
+ String p = c.getPath();
+ if (seen.contains(p)) {
+ continue;
+ }
+ if (secondSet.remove(p) != null) {
+ current = c;
+ markSeen(p);
+ return;
+ }
+ while (second.hasNext()) {
+ IndexRow s = second.next();
+ String p2 = s.getPath();
+ if (p.equals(p2)) {
+ current = c;
+ markSeen(p);
+ return;
+ }
+ secondSet.put(p2, s);
+ FilterIterators.checkMemoryLimit(secondSet.size(), settings);
+ }
+ }
+ }
+
+ private void markSeen(String path) {
+ seen.add(path);
+ FilterIterators.checkMemoryLimit(seen.size(), settings);
+ }
+
+ @Override
+ public long getSize(SizePrecision precision, long max) {
+ // this is the worst case
+ long a = first.getSize(precision, max);
+ long b = second.getSize(precision, max);
+ if (a < 0 || b < 0) {
+ return -1;
+ }
+ return QueryImpl.saturatedAdd(a, b);
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java
new file mode 100644
index 0000000000..b071abb1ea
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java
@@ -0,0 +1,73 @@
+/*
+ * 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.cursor;
+
+import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
+
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterators;
+
+/**
+ * <code>PathCursor</code> implements a simple {@link Cursor} that iterates
+ * over a {@link String} based path {@link Iterable}.
+ */
+public class PathCursor extends AbstractCursor {
+
+ private final Iterator<String> iterator;
+
+ public PathCursor(Iterator<String> paths, boolean distinct, final QueryLimits settings) {
+ Iterator<String> it = paths;
+ if (distinct) {
+ it = Iterators.filter(it, new Predicate<String>() {
+
+ private final HashSet<String> known = new HashSet<String>();
+
+ @Override
+ public boolean apply(@Nullable String input) {
+ FilterIterators.checkMemoryLimit(known.size(), settings);
+ // Set.add returns true for new entries
+ return known.add(input);
+ }
+
+ });
+ }
+ this.iterator = it;
+ }
+
+ @Override
+ public IndexRow next() {
+ // TODO support jcr:score and possibly rep:excerpt
+ String path = iterator.next();
+ return new IndexRowImpl(isAbsolute(path) ? path : "/" + path);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java
new file mode 100644
index 0000000000..2f5d77ef54
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java
@@ -0,0 +1,203 @@
+/*
+ * 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.cursor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterators;
+
+/**
+ * A cursor that is able to prefetch nodes from the node store.
+ */
+public class PrefetchCursor extends AbstractCursor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PrefetchCursor.class);
+
+ // match ${...} -- unfortunately it needs a lot of escaping.
+ // } inside {...} need to be escaped: ${...\}...}
+ private static final Pattern FUNCTION = Pattern.compile("\\$\\{((\\}|[^}])*)\\}");
+
+ private final Cursor cursor;
+ private final PrefetchNodeStore store;
+ private final int prefetchCount;
+ private final NodeState rootState;
+ private Iterator<IndexRow> prefetched;
+ private final List<String> prefetchRelative;
+
+ PrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount, NodeState rootState, List<String> prefetchRelative) {
+ this.cursor = cursor;
+ this.store = store;
+ this.prefetchCount = prefetchCount;
+ this.rootState = rootState;
+ this.prefetched = Iterators.emptyIterator();
+ this.prefetchRelative = prefetchRelative;
+ }
+
+ @Override
+ public IndexRow next() {
+ if (!prefetched.hasNext()) {
+ ArrayList<IndexRow> rows = new ArrayList<>();
+ TreeSet<String> paths = new TreeSet<>();
+ for (int i = 0; i < prefetchCount && cursor.hasNext(); i++) {
+ IndexRow row = cursor.next();
+ rows.add(row);
+ if (row.isVirtualRow()) {
+ continue;
+ }
+ String p = row.getPath();
+ if (!PathUtils.isAbsolute(p)) {
+ LOG.warn("Unexpected relative path {}", p);
+ continue;
+ }
+ prefetchRelative(paths, p);
+ do {
+ paths.add(p);
+ p = PathUtils.getParentPath(p);
+ } while (!PathUtils.denotesRoot(p));
+ }
+ store.prefetch(paths, rootState);
+ prefetched = rows.iterator();
+ }
+ return prefetched.next();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return prefetched.hasNext() || cursor.hasNext();
+ }
+
+ private void prefetchRelative(Set<String> target, String p) {
+ try {
+ for (String r : prefetchRelative) {
+ String result = resolve(r, p);
+ if (result != null) {
+ target.add(result);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ }
+
+ /**
+ * Try to compute an absolute path using a relative path specification.
+ *
+ * @param relativePath a relative path that may include patterns, e.g. jcr:content/metadata,
+ * or jcr:content/renditions/${regex(".+/(.*)\\..*")}
+ * @param path the input path, e.g. /content/images/abc
+ * @return null if not a match, or the output path, e.g.
+ * /content/images/abc.png/jcr:content/renditions/abc
+ * @throws IllegalArgumentException if an error occurs
+ */
+ public static String resolve(String relativePath, String path) throws IllegalArgumentException {
+ String resolved = applyAndResolvePatterns(relativePath, path);
+ if (resolved == null) {
+ return null;
+ }
+ if (PathUtils.isAbsolute(resolved)) {
+ return resolved;
+ }
+ return PathUtils.concat(path, resolved);
+ }
+
+ /**
+ * Compute the new relative path from a input path and a relative path (which
+ * may include patterns).
+ *
+ * @param relativePath the value that may include patterns, e.g.
+ * jcr:content/metadata, or
+ * jcr:content/renditions/${regex(".+/(.*)\\..*")}
+ * @param path the input path, e.g. /content/images/abc
+ * @return null if not a match, or the output (relative) path, e.g.
+ * jcr:content/renditions/abc
+ * @throws IllegalArgumentException if an error occurs
+ */
+ public static String applyAndResolvePatterns(String relativePath, String path)
+ throws IllegalArgumentException {
+ Matcher m = FUNCTION.matcher(relativePath);
+ // appendReplacement with StringBuilder is not supported in Java 8
+ StringBuffer buff = new StringBuffer();
+ // iterate over all the "${...}"
+ try {
+ while (m.find()) {
+ String f = m.group(1);
+ // un-escape: \} => }
+ f = f.replaceAll("\\\\}", "}");
+ // we only support regex("...") currently
+ if (f.startsWith("regex(\"") && f.endsWith("\")")) {
+ String regexPattern = f.substring("regex(\"".length(), f.length() - 2);
+ // un-escape: \" => "
+ regexPattern = regexPattern.replaceAll("\\\"", "\"");
+ String x = findMatch(path, regexPattern);
+ if (x == null) {
+ // not a match
+ return null;
+ }
+ m.appendReplacement(buff, x);
+ } else {
+ // don't know how to replace - don't
+ // (we could also throw an exception)
+ break;
+ }
+ }
+ } catch (IllegalStateException | IndexOutOfBoundsException e) {
+ LOG.warn("Can not replace {} match in {}", relativePath, path, e);
+ throw new IllegalArgumentException("Can not replace");
+ }
+ m.appendTail(buff);
+ return buff.toString();
+ }
+
+ /**
+ * Find a pattern in a given path.
+ *
+ * @param path the absolute path, e.g. "/content/images/hello.jpg"
+ * @param regexPattern the pattern, e.g. ".+/(.*).jpg"
+ * @return null if not found, or the matched group, e.g. "hello"
+ * @throws IllegalArgumentException if an error occurs
+ */
+ public static String findMatch(String path, String regexPattern)
+ throws IllegalArgumentException {
+ try {
+ Pattern p = Pattern.compile(regexPattern);
+ Matcher m = p.matcher(path);
+ if (!m.find()) {
+ return null;
+ }
+ return m.group(1);
+ } catch (PatternSyntaxException | IllegalStateException | IndexOutOfBoundsException e) {
+ LOG.warn("Can not find match in {} pattern {}", path, regexPattern, e);
+ throw new IllegalArgumentException("Can not find match", e);
+ }
+ }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java
new file mode 100644
index 0000000000..0432a762aa
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java
@@ -0,0 +1,192 @@
+/*
+ * 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.cursor;
+
+import static org.apache.jackrabbit.oak.spi.query.QueryConstants.REP_FACET;
+
+import java.util.Deque;
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Queues;
+
+/**
+ * A cursor that reads all nodes in a given subtree.
+ */
+class TraversingCursor extends AbstractCursor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TraversingCursor.class);
+
+ private final Filter filter;
+
+ private final Deque<Iterator<? extends ChildNodeEntry>> nodeIterators =
+ Queues.newArrayDeque();
+
+ private String parentPath;
+
+ private String currentPath;
+
+ private long readCount;
+
+ private boolean init;
+
+ private boolean closed;
+
+ private final QueryLimits settings;
+
+ public TraversingCursor(Filter filter, NodeState rootState) {
+ this.filter = filter;
+ this.settings = filter.getQueryLimits();
+
+ String path = filter.getPath();
+ parentPath = null;
+ currentPath = "/";
+ NodeState parent = null;
+ NodeState node = rootState;
+
+ if (filter.containsNativeConstraint()) {
+ // OAK-4313: if no other index was found,
+ // then, for native queries, we won't match anything
+ return;
+ }
+
+ if (filter.isAlwaysFalse()) {
+ // nothing can match this filter, leave nodes empty
+ return;
+ }
+
+ Filter.PropertyRestriction facetRestriction = filter.getPropertyRestriction(REP_FACET);
+ if (facetRestriction != null) {
+ // we don't evaluate facets by traversal
+ throw new IllegalArgumentException(facetRestriction + " can't be evaluated by traversal");
+ }
+
+ if (!path.equals("/")) {
+ for (String name : path.substring(1).split("/")) {
+ parentPath = currentPath;
+ currentPath = PathUtils.concat(parentPath, name);
+
+ parent = node;
+ node = parent.getChildNode(name);
+ }
+ if (!node.exists()) {
+ // nothing can match this filter, leave nodes empty
+ return;
+ }
+ }
+ Filter.PathRestriction restriction = filter.getPathRestriction();
+ switch (restriction) {
+ case NO_RESTRICTION:
+ case EXACT:
+ case ALL_CHILDREN:
+ nodeIterators.add(Iterators.singletonIterator(
+ new MemoryChildNodeEntry(currentPath, node)));
+ parentPath = "";
+ break;
+ case PARENT:
+ if (parent != null) {
+ nodeIterators.add(Iterators.singletonIterator(
+ new MemoryChildNodeEntry(parentPath, parent)));
+ parentPath = "";
+ }
+ break;
+ case DIRECT_CHILDREN:
+ nodeIterators.add(node.getChildNodeEntries().iterator());
+ parentPath = currentPath;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown restriction: " + restriction);
+ }
+ }
+
+ @Override
+ public IndexRow next() {
+ if (closed) {
+ throw new IllegalStateException("This cursor is closed");
+ }
+ if (!init) {
+ fetchNext();
+ init = true;
+ }
+ IndexRowImpl result = new IndexRowImpl(currentPath);
+ fetchNext();
+ return result;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!closed && !init) {
+ fetchNext();
+ init = true;
+ }
+ return !closed;
+ }
+
+ private void fetchNext() {
+ while (!nodeIterators.isEmpty()) {
+ Iterator<? extends ChildNodeEntry> iterator = nodeIterators.getLast();
+ if (iterator.hasNext()) {
+ ChildNodeEntry entry = iterator.next();
+
+ readCount++;
+ if (readCount % 1000 == 0) {
+ FilterIterators.checkReadLimit(readCount, settings);
+ String caller = IndexUtils.getCaller(this.settings.getIgnoredClassNamesInCallTrace());
+ LOG.warn("Traversed {} nodes with filter {} called by {}; consider creating an index or changing the query" ,
+ readCount, filter, caller);
+ }
+
+ NodeState node = entry.getNodeState();
+
+ String name = entry.getName();
+ if (NodeStateUtils.isHidden(name)) {
+ continue;
+ }
+ currentPath = PathUtils.concat(parentPath, name);
+
+ PathRestriction r = filter.getPathRestriction();
+ if (r == PathRestriction.ALL_CHILDREN ||
+ r == PathRestriction.NO_RESTRICTION) {
+ nodeIterators.addLast(node.getChildNodeEntries().iterator());
+ parentPath = currentPath;
+ }
+ return;
+ } else {
+ nodeIterators.removeLast();
+ parentPath = PathUtils.getParentPath(parentPath);
+ }
+ }
+ currentPath = null;
+ closed = true;
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
index de271248fb..fb7bb9856a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
@@ -16,8 +16,8 @@
*/
package org.apache.jackrabbit.oak.plugins.index.diffindex;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.query.QueryIndex;
import org.apache.jackrabbit.oak.spi.state.NodeState;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
index 1997f0a6f2..672158d620 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
@@ -19,12 +19,11 @@
package org.apache.jackrabbit.oak.plugins.index.nodetype;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
-
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.api.Type;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.query.QueryIndex;
import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
index 10664bd3a5..a00347b88e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
@@ -28,9 +28,9 @@ import java.util.List;
import java.util.Set;
import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
import org.apache.jackrabbit.oak.spi.filter.PathFilter;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
index f00b19955f..864f524df1 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
@@ -25,10 +25,10 @@ import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.cursor.Cursors.newPathCursor;
import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.NAME;
import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.REF_NAME;
import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.WEAK_REF_NAME;
-import static org.apache.jackrabbit.oak.plugins.index.Cursors.newPathCursor;
import java.util.ArrayList;
import java.util.List;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
index db1daaa6ee..8610e770c4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
@@ -49,8 +49,8 @@ import org.apache.jackrabbit.oak.query.index.FilterImpl;
import org.apache.jackrabbit.oak.query.plan.ExecutionPlan;
import org.apache.jackrabbit.oak.query.plan.SelectorExecutionPlan;
import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.spi.query.IndexRow;
import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
import org.apache.jackrabbit.oak.spi.query.QueryConstants;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
index cad4f4d847..b61ace1eb1 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
@@ -19,8 +19,8 @@
package org.apache.jackrabbit.oak.query.index;
import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.plugins.index.counter.jmx.NodeCounter;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.query.ast.JoinConditionImpl;
import org.apache.jackrabbit.oak.spi.query.Cursor;
import org.apache.jackrabbit.oak.spi.query.Filter;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java
new file mode 100644
index 0000000000..870a670ddb
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cursor;
+
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+
+public class CursorUtils {
+
+ public static String toString(Cursor cursor) {
+ StringBuilder buff = new StringBuilder();
+ while (cursor.hasNext()) {
+ if (buff.length() > 0) {
+ buff.append(", ");
+ }
+ buff.append(cursor.next().getPath());
+ }
+ return buff.toString();
+ }
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
similarity index 98%
rename from oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java
rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
index 99d7264134..f46e30df62 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.jackrabbit.oak.plugins.index;
+package org.apache.jackrabbit.oak.plugins.index.cursor;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java
new file mode 100644
index 0000000000..1b427a7659
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.cursor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+public class PrefetchCursorTest {
+
+ @Test
+ public void findMatch() {
+ // find the file name, but only if it's a jpg
+ assertEquals("hello", PrefetchCursor.findMatch(
+ "/content/images/hello.jpg",
+ ".+/(.*)\\.jpg"));
+ // it will find "null" it it's not a jpg
+ assertNull(PrefetchCursor.findMatch(
+ "/content/images/hello.tiff",
+ ".+/(.*)\\.jpg"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void findMatchException() {
+ PrefetchCursor.findMatch(
+ "/content/images/hello.tiff",
+ "***");
+ }
+
+ @Test
+ public void applyAndResolvePatterns() {
+ assertEquals("jcr:content/metadata",
+ PrefetchCursor.applyAndResolvePatterns(
+ "jcr:content/metadata",
+ "/content/images/abc"));
+ assertEquals("jcr:content/renditions/abc",
+ PrefetchCursor.applyAndResolvePatterns(
+ "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}",
+ "/content/images/abc.jpg"));
+ assertNull(
+ PrefetchCursor.applyAndResolvePatterns(
+ "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}",
+ "/content/images/abc.tiff"));
+ assertEquals("jcr:content/renditions/abc",
+ PrefetchCursor.applyAndResolvePatterns(
+ "jcr:content/renditions/${regex(\"\\{(.*)\\\\}\")}",
+ "/content/images/{abc}/def"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void applyAndResolvePatternsException() {
+ PrefetchCursor.applyAndResolvePatterns(
+ "jcr:content/${regex(\"***\")}",
+ "/content/images/abc");
+ }
+
+ @Test
+ public void resolve() {
+ assertEquals("/content/images/abc/jcr:content/metadata",
+ PrefetchCursor.resolve(
+ "jcr:content/metadata",
+ "/content/images/abc"));
+ assertEquals("/content/images/abc.jpg/jcr:content/renditions/abc.xml",
+ PrefetchCursor.resolve(
+ "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml",
+ "/content/images/abc.jpg"));
+ assertNull(
+ PrefetchCursor.resolve(
+ "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml",
+ "/content/images/abc.tiff"));
+ assertEquals("/fileTypes/jpg",
+ PrefetchCursor.resolve(
+ "/fileTypes/${regex(\".+/.*\\.(.*)\")}",
+ "/content/images/abc.jpg"));
+ }
+
+ @Test
+ public void cursor() {
+ List<String> paths = Arrays.asList(
+ "/test/a.jpg",
+ "/test/b.tiff",
+ "/test/c.pdf");
+ TestCursor cursor = new TestCursor(paths.iterator());
+ TestPrefetchNodeStore ns = new TestPrefetchNodeStore();
+ NodeState rootState = null;
+ List<String> prefetchRelative = Arrays.asList(
+ "jcr:content/metadata",
+ "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml",
+ "/fileTypes/${regex(\".+/.*\\.(.*)\")}");
+ PrefetchCursor pc = new PrefetchCursor(cursor, ns, 10, rootState, prefetchRelative);
+ assertEquals("/test/a.jpg, /test/b.tiff, /test/c.pdf",
+ CursorUtils.toString(pc));
+ assertEquals("[/fileTypes/jpg, /fileTypes/pdf, /fileTypes/tiff, " +
+ "/test, /test/a.jpg, /test/a.jpg/jcr:content/metadata, " +
+ "/test/a.jpg/jcr:content/renditions/a.xml, " +
+ "/test/b.tiff, /test/b.tiff/jcr:content/metadata, " +
+ "/test/c.pdf, /test/c.pdf/jcr:content/metadata]", ns.toString());
+ }
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.java
new file mode 100644
index 0000000000..26462cc224
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.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.cursor;
+
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+
+public class TestCursor extends AbstractCursor {
+
+ private final Iterator<String> it;
+
+ TestCursor(Iterator<String> it) {
+ this.it = it;
+ }
+
+ @Override
+ public IndexRow next() {
+ return new TestRow(it.next());
+ }
+
+ @Override
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.java
new file mode 100644
index 0000000000..0ca500311a
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.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.cursor;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+
+public class TestPrefetchNodeStore implements PrefetchNodeStore {
+
+ private final TreeSet<String> prefetched = new TreeSet<String>();
+
+ @Override
+ public void prefetch(Collection<String> paths, NodeState rootState) {
+ prefetched.addAll(paths);
+ }
+
+ public void reset() {
+ prefetched.clear();
+ }
+
+ public String toString() {
+ return prefetched.toString();
+ }
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.java
new file mode 100644
index 0000000000..fe6d74623e
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.java
@@ -0,0 +1,45 @@
+/*
+ * 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.cursor;
+
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+
+public class TestRow implements IndexRow {
+
+ private final String path;
+
+ TestRow(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public PropertyValue getValue(String arg0) {
+ return null;
+ }
+
+ @Override
+ public boolean isVirtualRow() {
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
index 8b23b8611e..9903cc5c00 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
@@ -28,6 +28,7 @@ import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
+import org.apache.jackrabbit.oak.plugins.index.cursor.PathCursor;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
@@ -40,7 +41,6 @@ import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EditorHook;
import org.apache.jackrabbit.oak.spi.mount.Mounts;
import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
@@ -102,7 +102,7 @@ public class NodeTypeIndexTest {
private static void checkCursor(Cursor cursor, String... matches) {
// make sure the index is actually used
// and does not traverse
- assertEquals(Cursors.class.getName() + "$PathCursor",
+ assertEquals(PathCursor.class.getName(),
cursor.getClass().getName());
Set<String> expected = Sets.newHashSet();
expected.addAll(Arrays.asList(matches));
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
index 450b24f3b8..d01b4bae46 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
@@ -25,9 +25,9 @@ import java.util.List;
import org.apache.jackrabbit.oak.AbstractSecurityTest;
import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.api.CommitFailedException;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexLookup;
import org.apache.jackrabbit.oak.query.QueryEngineSettings;