Author: mreutegg
Date: Wed Mar 26 20:34:36 2014
New Revision: 1582045

URL: http://svn.apache.org/r1582045
Log:
OAK-1342: Cascading document history

Modified:
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/PropertyHistory.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Range.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/ValueMap.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentSplitTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/RangeTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ValueMapTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/UtilsTest.java

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
 Wed Mar 26 20:34:36 2014
@@ -1298,7 +1298,7 @@ public final class DocumentNodeStore
             if (isDisposed.get()) {
                 return;
             }
-            LOG.warn("Background operation failed: " + e.toString(), e);
+            throw e;
         }
     }
 
@@ -1622,7 +1622,11 @@ public final class DocumentNodeStore
                 }
                 DocumentNodeStore nodeStore = ref.get();
                 if (nodeStore != null) {
-                    nodeStore.runBackgroundOperations();
+                    try {
+                        nodeStore.runBackgroundOperations();
+                    } catch (Throwable t) {
+                        LOG.warn("Background operation failed: " + 
t.toString(), t);
+                    }
                     delay = nodeStore.getAsyncDelay();
                 } else {
                     // node store not in use anymore

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeDocument.java
 Wed Mar 26 20:34:36 2014
@@ -44,6 +44,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -92,6 +93,12 @@ public final class NodeDocument extends 
     static final float SPLIT_RATIO = 0.3f;
 
     /**
+     * Create an intermediate previous document when there are this many
+     * previous documents of equal height.
+     */
+    static final int PREV_SPLIT_FACTOR = 10;
+
+    /**
      * Revision collision markers set by commits with modifications, which
      * overlap with un-merged branch commits.
      * Key: revision, value: always true
@@ -169,8 +176,9 @@ public final class NodeDocument extends 
     /**
      * Properties to ignore when a document is split.
      */
-    private static final Set<String> IGNORE_ON_SPLIT = ImmutableSet.of(ID, 
MOD_COUNT, MODIFIED, PREVIOUS,
-            LAST_REV, CHILDREN_FLAG, HAS_BINARY_FLAG);
+    private static final Set<String> IGNORE_ON_SPLIT = ImmutableSet.of(
+            ID, MOD_COUNT, MODIFIED, PREVIOUS, LAST_REV, CHILDREN_FLAG,
+            HAS_BINARY_FLAG, PATH);
 
     public static final long HAS_BINARY_VAL = 1;
 
@@ -195,7 +203,7 @@ public final class NodeDocument extends 
     /**
      * Required for serialization
      *
-     * @param store
+     * @param store the document store.
      * @param creationTime time at which it was created. Would be different 
from current time
      *                     in case of being resurrected from a serialized for
      */
@@ -215,8 +223,7 @@ public final class NodeDocument extends 
      */
     @Nonnull
     public Map<Revision, String> getValueMap(@Nonnull String key) {
-        Object value = super.get(key);
-        if (IGNORE_ON_SPLIT.contains(key) || !(value instanceof Map)) {
+        if (IGNORE_ON_SPLIT.contains(key)) {
             return Collections.emptyMap();
         } else {
             return ValueMap.create(this, key);
@@ -236,7 +243,7 @@ public final class NodeDocument extends 
      */
     public boolean hasChildren() {
         Boolean childrenFlag = (Boolean) get(CHILDREN_FLAG);
-        return childrenFlag != null ? childrenFlag.booleanValue() : false;
+        return childrenFlag != null && childrenFlag;
     }
 
     /**
@@ -278,6 +285,27 @@ public final class NodeDocument extends 
     }
 
     /**
+     * Returns the path of the main document if this document is part of a 
_prev
+     * history tree. Otherwise this method simply returns {@link #getPath()}.
+     *
+     * @return the path of the main document.
+     */
+    @Nonnull
+    public String getMainPath() {
+        String p = getPath();
+        if (p.startsWith("p")) {
+            p = PathUtils.getAncestorPath(p, 2);
+            if (p.length() == 1) {
+                return "/";
+            } else {
+                return p.substring(1);
+            }
+        } else {
+            return p;
+        }
+    }
+
+    /**
      * @return a map of the last known revision for each clusterId.
      */
     @Nonnull
@@ -658,17 +686,24 @@ public final class NodeDocument extends 
      */
     @Nonnull
     public Iterable<UpdateOp> split(@Nonnull RevisionContext context) {
+        SortedMap<Revision, Range> previous = getPreviousRanges();
         // only consider if there are enough commits,
         // unless document is really big
         if (getLocalRevisions().size() + getLocalCommitRoot().size() <= 
NUM_REVS_THRESHOLD
-                && getMemory() < DOC_SIZE_THRESHOLD) {
+                && getMemory() < DOC_SIZE_THRESHOLD
+                && previous.size() < PREV_SPLIT_FACTOR) {
             return Collections.emptyList();
         }
+        String path = getPath();
         String id = getId();
-        SortedMap<Revision, Range> previous = getPreviousRanges();
+        if (id == null) {
+            throw new IllegalStateException("document does not have an id: " + 
this);
+        }
         // what's the most recent previous revision?
         Revision recentPrevious = null;
-        for (Revision rev : previous.keySet()) {
+        Map<Integer, List<Range>> prevHisto = Maps.newHashMap();
+        for (Map.Entry<Revision, Range> entry : previous.entrySet()) {
+            Revision rev = entry.getKey();
             if (rev.getClusterId() != context.getClusterId()) {
                 continue;
             }
@@ -676,6 +711,13 @@ public final class NodeDocument extends 
                     || isRevisionNewer(context, rev, recentPrevious)) {
                 recentPrevious = rev;
             }
+            Range r = entry.getValue();
+            List<Range> list = prevHisto.get(r.getHeight());
+            if (list == null) {
+                list = new ArrayList<Range>();
+                prevHisto.put(r.getHeight(), list);
+            }
+            list.add(r);
         }
         Map<String, NavigableMap<Revision, String>> splitValues
                 = new HashMap<String, NavigableMap<Revision, String>>();
@@ -703,7 +745,7 @@ public final class NodeDocument extends 
             }
         }
 
-        List<UpdateOp> splitOps = Collections.emptyList();
+        List<UpdateOp> splitOps = Lists.newArrayList();
         int numValues = 0;
         Revision high = null;
         Revision low = null;
@@ -724,19 +766,20 @@ public final class NodeDocument extends 
             }
             numValues += splitMap.size();
         }
+        UpdateOp main = null;
         if (high != null && low != null
                 && (numValues >= NUM_REVS_THRESHOLD
                     || getMemory() > DOC_SIZE_THRESHOLD)) {
             // enough revisions to split off
-            splitOps = new ArrayList<UpdateOp>(2);
             // move to another document
-            UpdateOp main = new UpdateOp(id, false);
-            setPrevious(main, high, low);
-            UpdateOp old = new UpdateOp(Utils.getPreviousIdFor(id, high), 
true);
-            if (get(PATH) != null) {
-                old.set(PATH, get(PATH));
-            }
+            main = new UpdateOp(id, false);
+            setPrevious(main, new Range(high, low, 0));
+            String oldPath = Utils.getPreviousPathFor(path, high, 0);
+            UpdateOp old = new UpdateOp(Utils.getIdFromPath(oldPath), true);
             old.set(ID, old.getId());
+            if (Utils.isLongPath(oldPath)) {
+                old.set(PATH, oldPath);
+            }
             for (String property : splitValues.keySet()) {
                 NavigableMap<Revision, String> splitMap = 
splitValues.get(property);
                 for (Map.Entry<Revision, String> entry : splitMap.entrySet()) {
@@ -751,9 +794,50 @@ public final class NodeDocument extends 
             // only split if half of the data can be moved to old document
             if (oldDoc.getMemory() > getMemory() * SPLIT_RATIO) {
                 splitOps.add(old);
-                splitOps.add(main);
             }
         }
+
+        // check if we need to create intermediate previous documents
+        for (Map.Entry<Integer, List<Range>> entry : prevHisto.entrySet()) {
+            if (entry.getValue().size() >= PREV_SPLIT_FACTOR) {
+                if (main == null) {
+                    main = new UpdateOp(id, false);
+                }
+                // calculate range new range
+                Revision h = null;
+                Revision l = null;
+                for (Range r : entry.getValue()) {
+                    if (h == null || isRevisionNewer(context, r.high, h)) {
+                        h = r.high;
+                    }
+                    if (l == null || isRevisionNewer(context, l, r.low)) {
+                        l = r.low;
+                    }
+                    removePrevious(main, r);
+                }
+                if (h == null || l == null) {
+                    throw new IllegalStateException();
+                }
+                String prevPath = Utils.getPreviousPathFor(path, h, 
entry.getKey() + 1);
+                String prevId = Utils.getIdFromPath(prevPath);
+                UpdateOp intermediate = new UpdateOp(prevId, true);
+                intermediate.set(ID, prevId);
+                if (Utils.isLongPath(prevPath)) {
+                    intermediate.set(PATH, prevPath);
+                }
+                setPrevious(main, new Range(h, l, entry.getKey() + 1));
+                for (Range r : entry.getValue()) {
+                    setPrevious(intermediate, r);
+                }
+                splitOps.add(intermediate);
+            }
+        }
+
+        // main document must be updated last
+        if (main != null && !splitOps.isEmpty()) {
+            splitOps.add(main);
+        }
+
         return splitOps;
     }
 
@@ -773,9 +857,8 @@ public final class NodeDocument extends 
                 NavigableMap<Revision, Range> transformed = new 
TreeMap<Revision, Range>(
                         StableRevisionComparator.REVERSE);
                 for (Map.Entry<Revision, String> entry : map.entrySet()) {
-                    Revision high = entry.getKey();
-                    Revision low = Revision.fromString(entry.getValue());
-                    transformed.put(high, new Range(high, low));
+                    Range r = Range.fromEntry(entry.getKey(), 
entry.getValue());
+                    transformed.put(r.high, r);
                 }
                 previous = Maps.unmodifiableNavigableMap(transformed);
             }
@@ -803,10 +886,13 @@ public final class NodeDocument extends 
         if (revision == null) {
             return new PropertyHistory(store, this, property);
         } else {
+            final String mainPath = getMainPath();
             // first try to lookup revision directly
-            Revision r = getPreviousRanges().floorKey(revision);
-            if (r != null) {
-                String prevId = Utils.getPreviousIdFor(getId(), r);
+            Map.Entry<Revision, Range> entry = 
getPreviousRanges().floorEntry(revision);
+            if (entry != null) {
+                Revision r = entry.getKey();
+                int h = entry.getValue().height;
+                String prevId = Utils.getPreviousIdFor(mainPath, r, h);
                 NodeDocument prev = store.find(Collection.NODES, prevId);
                 if (prev != null) {
                     if (prev.getValueMap(property).containsKey(revision)) {
@@ -824,7 +910,8 @@ public final class NodeDocument extends 
                 public NodeDocument apply(Map.Entry<Revision, Range> input) {
                     if (input.getValue().includes(revision)) {
                         Revision r = input.getKey();
-                        String prevId = Utils.getPreviousIdFor(getId(), r);
+                        int h = input.getValue().height;
+                        String prevId = Utils.getPreviousIdFor(mainPath, r, h);
                         NodeDocument prev = store.find(Collection.NODES, 
prevId);
                         if (prev != null) {
                             return prev;
@@ -881,7 +968,7 @@ public final class NodeDocument extends 
 
     public static void setChildrenFlag(@Nonnull UpdateOp op,
                                        boolean hasChildNode) {
-        checkNotNull(op).set(CHILDREN_FLAG, Boolean.valueOf(hasChildNode));
+        checkNotNull(op).set(CHILDREN_FLAG, hasChildNode);
     }
 
     public static void setModified(@Nonnull UpdateOp op,
@@ -957,10 +1044,14 @@ public final class NodeDocument extends 
     }
 
     public static void setPrevious(@Nonnull UpdateOp op,
-                                   @Nonnull Revision high,
-                                   @Nonnull Revision low) {
-        checkNotNull(op).setMapEntry(PREVIOUS, checkNotNull(high),
-                checkNotNull(low).toString());
+                                   @Nonnull Range range) {
+        checkNotNull(op).setMapEntry(PREVIOUS, checkNotNull(range).high,
+                range.getLowValue());
+    }
+
+    public static void removePrevious(@Nonnull UpdateOp op,
+                                      @Nonnull Range range) {
+        checkNotNull(op).removeMapEntry(PREVIOUS, checkNotNull(range).high);
     }
 
     public static void setHasBinary(@Nonnull UpdateOp op) {
@@ -1192,11 +1283,6 @@ public final class NodeDocument extends 
         return ValueMap.create(this, DELETED);
     }
 
-    @Nonnull
-    private Map<Revision, String> getCommitRoot() {
-        return ValueMap.create(this, COMMIT_ROOT);
-    }
-
     /**
      * The list of children for a node. The list might be complete or not, in
      * which case it only represents a block of children.

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/PropertyHistory.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/PropertyHistory.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/PropertyHistory.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/PropertyHistory.java
 Wed Mar 26 20:34:36 2014
@@ -46,29 +46,30 @@ class PropertyHistory implements Iterabl
     private static final Logger LOG = 
LoggerFactory.getLogger(PropertyHistory.class);
 
     private final DocumentStore store;
-    private final NodeDocument main;
+    private final NodeDocument doc;
     private final String property;
-    private final String id;
+    // path of the main document
+    private final String mainPath;
 
     public PropertyHistory(@Nonnull DocumentStore store,
-                           @Nonnull NodeDocument main,
+                           @Nonnull NodeDocument doc,
                            @Nonnull String property) {
         this.store = checkNotNull(store);
-        this.main = checkNotNull(main);
+        this.doc = checkNotNull(doc);
         this.property = checkNotNull(property);
-        this.id = main.getId();
+        this.mainPath = doc.getMainPath();
     }
 
     @Override
     public Iterator<NodeDocument> iterator() {
-        return ensureOrder(filter(
-                transform(main.getPreviousRanges().entrySet(),
-                        new Function<Map.Entry<Revision, Range>, 
Map.Entry<Revision, NodeDocument>>() {
+        return ensureOrder(filter(transform(doc.getPreviousRanges().entrySet(),
+                new Function<Map.Entry<Revision, Range>, Map.Entry<Revision, 
NodeDocument>>() {
             @Nullable
             @Override
             public Map.Entry<Revision, NodeDocument> apply(Map.Entry<Revision, 
Range> input) {
                 Revision r = input.getKey();
-                String prevId = Utils.getPreviousIdFor(id, r);
+                int h = input.getValue().height;
+                String prevId = Utils.getPreviousIdFor(mainPath, r, h);
                 NodeDocument prev = store.find(Collection.NODES, prevId);
                 if (prev == null) {
                     LOG.warn("Document with previous revisions not found: " + 
prevId);
@@ -155,10 +156,11 @@ class PropertyHistory implements Iterabl
                     // check if the revision is actually in there
                     if (doc != null) {
                         Map<Revision, String> values = 
doc.getValueMap(property);
-                        if (!values.isEmpty()) {
+                        Iterator<Revision> revs = values.keySet().iterator();
+                        if (revs.hasNext()) {
                             // put into queue with first (highest) revision
                             // from value map
-                            queue.put(values.keySet().iterator().next(), doc);
+                            queue.put(revs.next(), doc);
                         }
                     }
                 }

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Range.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Range.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Range.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Range.java
 Wed Mar 26 20:34:36 2014
@@ -28,6 +28,7 @@ final class Range {
 
     final Revision high;
     final Revision low;
+    final int height;
 
     /**
      * A range of revisions, with both inclusive bounds.
@@ -35,13 +36,49 @@ final class Range {
      * @param high the high bound.
      * @param low the low bound.
      */
-    Range(@Nonnull Revision high, @Nonnull Revision low) {
+    Range(@Nonnull Revision high, @Nonnull Revision low, int height) {
         this.high = checkNotNull(high);
         this.low = checkNotNull(low);
+        this.height = height;
         checkArgument(high.getClusterId() == low.getClusterId(),
                 "Revisions from have the same clusterId");
         checkArgument(high.compareRevisionTime(low) >= 0,
                 "High Revision must be later than low Revision, high=" + high 
+ " low=" + low);
+        checkArgument(height >= 0);
+    }
+
+    /**
+     * Creates a {@code Range} from a revisioned document entry.
+     *
+     * @param rev the revision of the entry corresponding to the high bound
+     *            of the range.
+     * @param value the string representation of the lower bound with the 
height
+     *              (e.g. r1-0-1/0).
+     * @return the range.
+     */
+    @Nonnull
+    static Range fromEntry(Revision rev, String value) {
+        Revision low;
+        int height;
+        int idx = value.indexOf('/');
+        if (idx == -1) {
+            // backward compatibility for lower bound without height
+            low = Revision.fromString(value);
+            height = 0;
+        } else {
+            low = Revision.fromString(value.substring(0, idx));
+            height = Integer.parseInt(value.substring(idx + 1));
+        }
+        return new Range(rev, low, height);
+    }
+
+    /**
+     * @return the string representation of the lower bound, including the
+     *         height (e.g. r1-0-1/0).
+     */
+    @Nonnull
+    String getLowValue() {
+        return low + "/" + height;
     }
 
     /**
@@ -57,8 +94,34 @@ final class Range {
                 && low.compareRevisionTime(r) <= 0;
     }
 
+    /**
+     * Returns the height of this range in the tree of previous documents. The
+     * range of a leaf document has height zero.
+     *
+     * @return the height.
+     */
+    int getHeight() {
+        return height;
+    }
+
     @Override
     public String toString() {
-        return low.toString();
+        return high.toString() + " : " + low.toString() + "/" + height;
+    }
+
+    @Override
+    public int hashCode() {
+        return high.hashCode() ^ low.hashCode() ^ height;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof Range) {
+            Range other = (Range) obj;
+            return high.equals(other.high)
+                    && low.equals(other.low)
+                    && height == other.height;
+        }
+        return false;
     }
 }

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/ValueMap.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/ValueMap.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/ValueMap.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/ValueMap.java
 Wed Mar 26 20:34:36 2014
@@ -30,6 +30,7 @@ import javax.annotation.Nonnull;
 
 import org.apache.jackrabbit.oak.plugins.document.util.MergeSortedIterators;
 
+import com.google.common.base.Objects;
 import com.google.common.collect.Iterators;
 
 /**
@@ -39,25 +40,32 @@ import com.google.common.collect.Iterato
 class ValueMap {
 
     static final SortedMap<Revision, String> EMPTY = 
Collections.unmodifiableSortedMap(
-            new TreeMap<Revision, String>(StableRevisionComparator.INSTANCE));
+            new TreeMap<Revision, String>(StableRevisionComparator.REVERSE));
 
     @Nonnull
     static Map<Revision, String> create(@Nonnull final NodeDocument doc,
-                @Nonnull final String property) {
+                                        @Nonnull final String property) {
         final SortedMap<Revision, String> map = doc.getLocalMap(property);
         if (doc.getPreviousRanges().isEmpty()) {
             return map;
         }
-        final Comparator<? super Revision> c = map.comparator();
-        final Iterator<NodeDocument> docs = Iterators.concat(
-                Iterators.singletonIterator(doc),
-                doc.getPreviousDocs(property, null).iterator());
         final Set<Map.Entry<Revision, String>> entrySet
                 = new AbstractSet<Map.Entry<Revision, String>>() {
 
             @Override
             @Nonnull
             public Iterator<Map.Entry<Revision, String>> iterator() {
+
+                final Comparator<? super Revision> c = map.comparator();
+                final Iterator<NodeDocument> docs;
+                if (map.isEmpty()) {
+                    docs = doc.getPreviousDocs(property, null).iterator();
+                } else {
+                    docs = Iterators.concat(
+                            Iterators.singletonIterator(doc),
+                            doc.getPreviousDocs(property, null).iterator());
+                }
+
                 return new MergeSortedIterators<Map.Entry<Revision, String>>(
                         new Comparator<Map.Entry<Revision, String>>() {
                             @Override
@@ -69,7 +77,18 @@ class ValueMap {
                 ) {
                     @Override
                     public Iterator<Map.Entry<Revision, String>> 
nextIterator() {
-                        return docs.hasNext() ? 
docs.next().getLocalMap(property).entrySet().iterator() : null;
+                        NodeDocument d = docs.hasNext() ? docs.next() : null;
+                        if (d == null) {
+                            return null;
+                        }
+                        Map<Revision, String> values;
+                        if (Objects.equal(d.getId(), doc.getId())) {
+                            // return local map for main document
+                            values = d.getLocalMap(property);
+                        } else {
+                            values = d.getValueMap(property);
+                        }
+                        return values.entrySet().iterator();
                     }
 
                     @Override
@@ -83,7 +102,7 @@ class ValueMap {
             public int size() {
                 int size = map.size();
                 for (NodeDocument prev : doc.getPreviousDocs(property, null)) {
-                    size += prev.getLocalMap(property).size();
+                    size += prev.getValueMap(property).size();
                 }
                 return size;
             }
@@ -108,7 +127,7 @@ class ValueMap {
                 }
                 Revision r = (Revision) key;
                 for (NodeDocument prev : doc.getPreviousDocs(property, r)) {
-                    value = prev.getLocalMap(property).get(key);
+                    value = prev.getValueMap(property).get(key);
                     if (value != null) {
                         return value;
                     }

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java
 Wed Mar 26 20:34:36 2014
@@ -283,23 +283,23 @@ public class Utils {
         return id.substring(index + 1);
     }
 
-    public static String getPreviousIdFor(String id, Revision r) {
-        StringBuilder sb = new StringBuilder(id.length() + REVISION_LENGTH + 
3);
-        int index = id.indexOf(':');
-        int depth = 0;
-        for (int i = 0; i < index; i++) {
-            depth *= 10;
-            depth += Character.digit(id.charAt(i), 10);
+    public static String getPreviousPathFor(String path, Revision r, int 
height) {
+        if (!PathUtils.isAbsolute(path)) {
+            throw new IllegalArgumentException("path must be absolute: " + 
path);
         }
-        sb.append(depth + 1).append(":p");
-        sb.append(id, index + 1, id.length());
+        StringBuilder sb = new StringBuilder(path.length() + REVISION_LENGTH + 
3);
+        sb.append("p").append(path);
         if (sb.charAt(sb.length() - 1) != '/') {
             sb.append('/');
         }
-        r.toStringBuilder(sb);
+        r.toStringBuilder(sb).append("/").append(height);
         return sb.toString();
     }
 
+    public static String getPreviousIdFor(String path, Revision r, int height) 
{
+        return getIdFromPath(getPreviousPathFor(path, r, height));
+    }
+
     /**
      * Deep copy of a map that may contain map values.
      *

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentSplitTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentSplitTest.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentSplitTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentSplitTest.java
 Wed Mar 26 20:34:36 2014
@@ -24,11 +24,14 @@ import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 
+import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.spi.blob.MemoryBlobStore;
 import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.junit.Test;
 
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
 import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
@@ -38,7 +41,7 @@ import static org.junit.Assert.assertNul
 import static org.junit.Assert.assertTrue;
 
 /**
- * Check correct splitting of documents (OAK-926).
+ * Check correct splitting of documents (OAK-926 & OAK-1342).
  */
 public class DocumentSplitTest extends BaseDocumentMKTest {
 
@@ -298,6 +301,97 @@ public class DocumentSplitTest extends B
         assertNotNull(node);
     }
 
+    @Test
+    public void cascadingSplit() {
+        cascadingSplit("/test/node");
+    }
+
+    @Test
+    public void cascadingSplitLongPath() {
+        String p = "/";
+        while (!Utils.isLongPath(p)) {
+            p = PathUtils.concat(p, "long-path-element");
+        }
+        cascadingSplit(p);
+    }
+
+    private void cascadingSplit(String path) {
+        // use a store without sync delay
+        mk.dispose();
+        mk = new DocumentMK.Builder().setAsyncDelay(0).open();
+        DocumentStore store = mk.getDocumentStore();
+        DocumentNodeStore ns = mk.getNodeStore();
+
+        String rev = null;
+        String p = "/";
+        for (String name : PathUtils.elements(path)) {
+            rev = mk.commit(p, "+\"" + name + "\":{}", rev, null);
+            p = PathUtils.concat(p, name);
+        }
+
+        List<String> revs = Lists.newArrayList();
+        for (int i = 0; i < NodeDocument.PREV_SPLIT_FACTOR + 1; i++) {
+            NodeDocument doc = store.find(NODES, Utils.getIdFromPath(path));
+            assertNotNull(doc);
+            assertEquals(i, doc.getPreviousRanges().size());
+            for (int j = 0; j < NodeDocument.NUM_REVS_THRESHOLD; j++) {
+                int value = (i * NodeDocument.NUM_REVS_THRESHOLD + j);
+                rev = mk.commit(path, "^\"prop\":" + value, rev, null);
+                revs.add(rev);
+            }
+            ns.runBackgroundOperations();
+        }
+        NodeDocument doc = store.find(NODES, Utils.getIdFromPath(path));
+        assertNotNull(doc);
+        assertEquals(2, doc.getPreviousRanges().size());
+        for (String s : revs) {
+            Revision r = Revision.fromString(s);
+            if (doc.getLocalRevisions().containsKey(r)) {
+                continue;
+            }
+            Iterable<NodeDocument> prev = doc.getPreviousDocs("prop", r);
+            assertEquals(1, Iterables.size(prev));
+            for (NodeDocument d : prev) {
+                assertTrue(d.containsRevision(r));
+            }
+        }
+
+        int numPrev = 0;
+        for (NodeDocument prev : doc.getPreviousDocs("prop", null)) {
+            numPrev++;
+            assertTrue(!prev.getValueMap("prop").isEmpty());
+        }
+        assertEquals(2, numPrev);
+
+        Revision previous = null;
+        int numValues = 0;
+        Map<Revision, String> valueMap = doc.getValueMap("prop");
+        for (Map.Entry<Revision, String> entry : valueMap.entrySet()) {
+            if (previous != null) {
+                assertTrue(ns.isRevisionNewer(previous, entry.getKey()));
+            }
+            previous = entry.getKey();
+            numValues++;
+            assertEquals(entry.getValue(), valueMap.get(entry.getKey()));
+        }
+        assertEquals(revs.size(), numValues);
+        assertEquals(revs.size(), valueMap.size());
+
+        assertNotNull(doc.getNodeAtRevision(ns, Revision.fromString(rev), 
null));
+    }
+
+    @Test
+    public void mainPath() {
+        Revision r = Revision.fromString("r1-0-1");
+        for (String path : new String[]{"/", "/test", "/test/path"}) {
+            DocumentStore store = mk.getDocumentStore();
+            NodeDocument doc = new NodeDocument(store);
+            String id = Utils.getPreviousIdFor(path, r, 0);
+            doc.put(NodeDocument.ID, id);
+            assertEquals(path, doc.getMainPath());
+        }
+    }
+
     private void syncMKs(List<DocumentMK> mks, int idx) {
         mks.get(idx).runBackgroundOperations();
         for (int i = 0; i < mks.size(); i++) {

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/RangeTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/RangeTest.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/RangeTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/RangeTest.java
 Wed Mar 26 20:34:36 2014
@@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.plugin
 
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -32,7 +33,7 @@ public class RangeTest {
         Revision r = new Revision(0x200, 0, 1);
         Revision other = new Revision(0x200, 0, 2);
 
-        Range range = new Range(high, low);
+        Range range = new Range(high, low, 0);
 
         // bounds are inclusive
         assertTrue(range.includes(low));
@@ -45,4 +46,13 @@ public class RangeTest {
         // other cluster id
         assertFalse(range.includes(other));
     }
+
+    @Test
+    public void parse() {
+        Revision low = Revision.fromString("r1-0-1");
+        Revision high = Revision.fromString("r2-0-1");
+        Range r = new Range(high, low, 0);
+        assertEquals("r1-0-1/0", r.getLowValue());
+        assertEquals(r, Range.fromEntry(high, r.getLowValue()));
+    }
 }

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ValueMapTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ValueMapTest.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ValueMapTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ValueMapTest.java
 Wed Mar 26 20:34:36 2014
@@ -41,18 +41,19 @@ public class ValueMapTest {
 
     @Test
     public void previousDocs1() {
-        String rootId = Utils.getIdFromPath("/");
+        String rootPath = "/";
+        String rootId = Utils.getIdFromPath(rootPath);
         Revision r0 = new Revision(0, 0, 1);
         MemoryDocumentStore store = new MemoryDocumentStore();
         // create previous docs
-        UpdateOp op = new UpdateOp(Utils.getPreviousIdFor(rootId, r0), true);
+        UpdateOp op = new UpdateOp(Utils.getPreviousIdFor(rootPath, r0, 0), 
true);
         op.set(ID, op.getId());
         op.setMapEntry("prop", r0, "0");
         NodeDocument.setRevision(op, r0, "c");
         store.createOrUpdate(NODES, op);
         Revision r1low = new Revision(1, 0, 1);
         Revision r1high = new Revision(1, 10, 1);
-        op = new UpdateOp(Utils.getPreviousIdFor(rootId, r1high), true);
+        op = new UpdateOp(Utils.getPreviousIdFor(rootPath, r1high, 0), true);
         op.set(ID, op.getId());
         for (int i = r1low.getCounter(); i <= r1high.getCounter(); i++) {
             Revision r = new Revision(1, i, 1);
@@ -66,8 +67,8 @@ public class ValueMapTest {
         Revision r2 = new Revision(2, 0, 1);
         op.setMapEntry("prop", r2, "1");
         NodeDocument.setRevision(op, r2, "c");
-        NodeDocument.setPrevious(op, r0, r0);
-        NodeDocument.setPrevious(op, r1high, r1low);
+        NodeDocument.setPrevious(op, new Range(r0, r0, 0));
+        NodeDocument.setPrevious(op, new Range(r1high, r1low, 0));
         store.createOrUpdate(NODES, op);
 
         NodeDocument doc = store.find(NODES, rootId);
@@ -86,7 +87,8 @@ public class ValueMapTest {
     @Test
     public void previousDocs2() {
         MemoryDocumentStore store = new MemoryDocumentStore();
-        String rootId = Utils.getIdFromPath("/");
+        String rootPath = "/";
+        String rootId = Utils.getIdFromPath(rootPath);
         Revision r01 = new Revision(0, 0, 1);
         Revision r12 = new Revision(1, 0, 2);
         Revision r22 = new Revision(2, 0, 2);
@@ -94,7 +96,7 @@ public class ValueMapTest {
         Revision r42 = new Revision(4, 0, 2);
         Revision r51 = new Revision(5, 0, 1);
         // create previous docs
-        UpdateOp op = new UpdateOp(Utils.getPreviousIdFor(rootId, r31), true);
+        UpdateOp op = new UpdateOp(Utils.getPreviousIdFor(rootPath, r31, 0), 
true);
         op.set(ID, op.getId());
         op.setMapEntry("p0", r01, "0");
         NodeDocument.setRevision(op, r01, "c");
@@ -102,7 +104,7 @@ public class ValueMapTest {
         NodeDocument.setRevision(op, r31, "c");
         store.createOrUpdate(NODES, op);
 
-        op = new UpdateOp(Utils.getPreviousIdFor(rootId, r42), true);
+        op = new UpdateOp(Utils.getPreviousIdFor(rootPath, r42, 0), true);
         op.set(ID, op.getId());
         op.setMapEntry("p1", r12, "0");
         NodeDocument.setRevision(op, r12, "c");
@@ -118,8 +120,8 @@ public class ValueMapTest {
         op.setMapEntry("p0", r51, "2");
         op.setMapEntry("p1", r51, "2");
         NodeDocument.setRevision(op, r51, "c");
-        NodeDocument.setPrevious(op, r42, r12);
-        NodeDocument.setPrevious(op, r31, r01);
+        NodeDocument.setPrevious(op, new Range(r42, r12, 0));
+        NodeDocument.setPrevious(op, new Range(r31, r01, 0));
         store.createOrUpdate(NODES, op);
 
         NodeDocument doc = store.find(NODES, rootId);
@@ -127,8 +129,8 @@ public class ValueMapTest {
         List<NodeDocument> prevDocs = Lists.newArrayList(
                 doc.getPreviousDocs("p1", null));
         assertEquals(2, prevDocs.size());
-        assertEquals(Utils.getPreviousIdFor(rootId, r31), 
prevDocs.get(0).getId());
-        assertEquals(Utils.getPreviousIdFor(rootId, r42), 
prevDocs.get(1).getId());
+        assertEquals(Utils.getPreviousIdFor(rootPath, r31, 0), 
prevDocs.get(0).getId());
+        assertEquals(Utils.getPreviousIdFor(rootPath, r42, 0), 
prevDocs.get(1).getId());
 
         List<Revision> revs = new ArrayList<Revision>();
         for (Revision r : doc.getValueMap("p1").keySet()) {

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/UtilsTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/UtilsTest.java?rev=1582045&r1=1582044&r2=1582045&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/UtilsTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/util/UtilsTest.java
 Wed Mar 26 20:34:36 2014
@@ -30,26 +30,26 @@ public class UtilsTest {
     @Test
     public void getPreviousIdFor() {
         Revision r = new Revision(System.currentTimeMillis(), 0, 0);
-        String p = Utils.getIdFromPath("/");
-        assertEquals("1:p/" + r.toString(), Utils.getPreviousIdFor(p, r));
-        p = Utils.getIdFromPath("/test");
-        assertEquals("2:p/test/" + r.toString(), Utils.getPreviousIdFor(p, r));
-        p = Utils.getIdFromPath("/a/b/c/d/e/f/g/h/i/j/k/l/m");
-        assertEquals("14:p/a/b/c/d/e/f/g/h/i/j/k/l/m/" + r.toString(), 
Utils.getPreviousIdFor(p, r));
+        assertEquals("2:p/" + r.toString() + "/0",
+                Utils.getPreviousIdFor("/", r, 0));
+        assertEquals("3:p/test/" + r.toString() + "/1",
+                Utils.getPreviousIdFor("/test", r, 1));
+        assertEquals("15:p/a/b/c/d/e/f/g/h/i/j/k/l/m/" + r.toString() + "/3",
+                Utils.getPreviousIdFor("/a/b/c/d/e/f/g/h/i/j/k/l/m", r, 3));
     }
 
     @Ignore("Performance test")
     @Test
     public void performance_getPreviousIdFor() {
         Revision r = new Revision(System.currentTimeMillis(), 0, 0);
-        String id = Utils.getIdFromPath("/some/test/path/foo");
+        String path = "/some/test/path/foo";
         // warm up
         for (int i = 0; i < 1 * 1000 * 1000; i++) {
-            Utils.getPreviousIdFor(id, r);
+            Utils.getPreviousIdFor(path, r, 0);
         }
         long time = System.currentTimeMillis();
         for (int i = 0; i < 10 * 1000 * 1000; i++) {
-            Utils.getPreviousIdFor(id, r);
+            Utils.getPreviousIdFor(path, r, 0);
         }
         time = System.currentTimeMillis() - time;
         System.out.println(time);


Reply via email to