You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by md...@apache.org on 2012/02/10 19:54:22 UTC

svn commit: r1242885 - in /jackrabbit/sandbox/jackrabbit-microkernel/src: main/java/org/apache/jackrabbit/state/ test/java/org/apache/jackrabbit/state/

Author: mduerig
Date: Fri Feb 10 18:54:21 2012
New Revision: 1242885

URL: http://svn.apache.org/viewvc?rev=1242885&view=rev
Log:
Microkernel based prototype of JCR implementation (WIP)
- refactored transient space implementation based on change log consolidation

Added:
    jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceFuzzTest.java
Modified:
    jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/ChangeTree.java
    jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/NodeState.java
    jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/TransientSpace.java
    jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceTest.java

Modified: jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/ChangeTree.java
URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/ChangeTree.java?rev=1242885&r1=1242884&r2=1242885&view=diff
==============================================================================
--- jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/ChangeTree.java (original)
+++ jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/ChangeTree.java Fri Feb 10 18:54:21 2012
@@ -24,39 +24,21 @@ import org.apache.jackrabbit.Path;
 import org.apache.jackrabbit.json.JsonValue;
 import org.apache.jackrabbit.spi.commons.iterator.Iterators;
 import org.apache.jackrabbit.spi.commons.iterator.Predicate;
-import org.apache.jackrabbit.utils.Predicates;
 
 import javax.jcr.ItemExistsException;
 import javax.jcr.ItemNotFoundException;
 import javax.jcr.PathNotFoundException;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 
 import static org.apache.commons.collections.map.ReferenceMap.HARD;
 import static org.apache.commons.collections.map.ReferenceMap.WEAK;
 import static org.apache.jackrabbit.utils.Unchecked.cast;
 
 /**
- * A change tree records changes to a tree of nodes and properties and it provides
- * a consolidated view of these changes. Changes are described by operations for
- * adding nodes, removing nodes, moving nodes and setting properties. <p/>
- *
- * A consolidate change list can be obtained from change trees. A change list is
- * a list of operations. A consolidated changes list is a change list which is
- * equivalent to the change list consisting of all operations applied to the change
- * tree and which is minimal amongst all change lists which are equivalent to the
- * former one. <p/>
- *
- * Two change lists are equivalent iff their effect on a tree of nodes and properties
- * is the same. That is, when applied to a tree both result in the same final state
- * of that tree. <p/>
- *
- * A change list is minimal amongst a set of change lists, if no change list in that
- * set contains more operations. <p/>
+ * A change tree records changes to a tree of nodes and properties. <p/>
  *
  * Internally a change tree is a tree of node deltas. A node delta describes whether
  * a node has been added, removed, moved or whether its properties have been changed.
@@ -66,40 +48,38 @@ import static org.apache.jackrabbit.util
  * removed or moved. <p/>
  *
  * A move operation is conceptually handled as a remove operation followed by an add
- * operation of the respective sub tree. In order to keep track of the move operation
- * the individual remove and add operations are marked as belonging to that move
- * operation. <p/>
- *
- * Since a remove node operation cancels a preceding add node operation for the same
- * node and because a move operation resembles a remove followed by an add operation
- * following equivalencies apply for move operations:
- *
- * <ul>
- * <li>Removing a node after it has been moved is equivalent to removing the original
- *   node.</li>
- * <li>Moving a node after it has been added is equivalent to adding the node at the
- *   target of the move.</li>
- * <li>Moving a moved node is equivalent to directly moving from the original source
- *   to the eventual target.</li>
- * </ul>
+ * operation of the respective sub tree. <p/>
  */
 public class ChangeTree {
     private final NodeDelta root;
     private final Predicate<Path> nodeExists;
+    private final Listener listener;
 
     /** Keep Existing instances at least as long as referenced by a client */
     private final Map<Path, Existing> existing = cast(new ReferenceMap(HARD, WEAK));
 
     /**
+     * Listener for modifications in the hierarchy
+     */
+    public interface Listener {
+        void added(NodeDelta nodeDelta);
+        void removed(NodeDelta nodeDelta);
+        void moved(Path source, NodeDelta nodeDelta);
+        void setValue(NodeDelta parent, String name, JsonValue value);
+    }
+
+    /**
      * Create a new change tree rooted at {@code rootPath}.
      * @param rootPath  root path for this change tree
+     * @param listener  listener for changes in the hierarchy
      * @param nodeExists  predicate which determines whether a path exists on the
      *                    persistent layer.
      */
-    public ChangeTree(final Path rootPath, Predicate<Path> nodeExists) {
+    public ChangeTree(final Path rootPath, Listener listener, Predicate<Path> nodeExists) {
         this.nodeExists = nodeExists;
-
-        root = new Existing(null, "") {
+        this.listener = listener;
+        
+        root = new Existing(null, "", rootPath) {
             @Override
             public Path getPath() {
                 return rootPath;
@@ -132,14 +112,6 @@ public class ChangeTree {
     }
 
     /**
-     * Collect all transient changes made to this change tree.
-     * @param listener
-     */
-    public void collectChanges(Listener listener) {
-        root.accept(new ChangeVisitor(listener));
-    }
-
-    /**
      * Clear all transient changes made to this change tree.
      */
     public void clear() {
@@ -150,161 +122,11 @@ public class ChangeTree {
     /**
      * @return  {@code true} iff this change tree has transient changes.
      */
-    public boolean hasChanges() {
+    public boolean hasChanges() {  
         return root.hasChanges();
     }
 
     /**
-     * @return A {@code Existing} instance for the given {@code parent} and {@code name}.
-     * Returns a previously allocated instance if not yet garbage collected.
-     * <em>Note:</em> returning fresh instances while previously allocated ones are still
-     * referenced in client code results in schizophrenia: same node multiple states.
-     */
-    private Existing existing(NodeDelta parent, String name) {
-        Path path = parent.getPath().concat(name);
-        Existing e = existing.get(path);
-        if (e == null) {
-            e = new Existing(parent, name);
-            existing.put(path, e);
-        }
-        return e;
-    }
-    
-    /**
-     * Visitor for traversing a change tree.
-     */
-    public interface Visitor {
-        void visit(Existing delta);
-        void visit(Added delta);
-        void visit(Removed delta);
-        void visit(Moved delta);
-    }
-
-    /**
-     * Listener for collecting changes in a change tree.
-     * @see ChangeTree#collectChanges(org.apache.jackrabbit.state.ChangeTree.Listener)
-     */
-    public interface Listener {
-
-        /**
-         * Record addition of a node at {@code path}.
-         * @param path
-         */
-        void addNode(Path path);
-
-        /**
-         * Record removal of a node at {@code path}.
-         * @param path
-         */
-        void removeNode(Path path);
-
-        /**
-         * Record a move from {@code sourcePath} to {@code destPath}.
-         * @param sourcePath
-         * @param destinationPath
-         */
-        void moveNode(Path sourcePath, Path destinationPath);
-
-        /**
-         * Record setting a property value at {@code path}.
-         * @param path
-         * @param value
-         */
-        void setProperty(Path path, JsonValue value);
-
-        /**
-         * Record removal of a property at {@code path}.
-         * @param path
-         */
-        void removeProperty(Path path);
-    }
-
-    private class ChangeVisitor implements Visitor {
-        private final Set<NodeDelta> visited = new HashSet<NodeDelta>();
-        private final Listener listener;
-
-        public ChangeVisitor(Listener listener) {
-            this.listener = listener;
-        }
-
-        @Override
-        public void visit(Existing delta) {
-            if (!visited.contains(delta)) {
-                visited.add(delta);
-                handleProperties(delta);
-                visitNodes(delta);
-            }
-        }
-
-        @Override
-        public void visit(Added delta) {
-            if (!visited.contains(delta)) {
-                listener.addNode(delta.getPath());
-                visited.add(delta);
-                handleProperties(delta);
-                visitNodes(delta);
-            }
-        }
-
-        private void visitParent(NodeDelta delta) {
-            if (!visited.contains(delta.parent)) {
-                visitParent(delta.parent);
-                delta.parent.accept(this);
-            }
-        }
-        
-        @Override
-        public void visit(Removed delta) {
-            if (!visited.contains(delta)) {
-                if (delta.isMoved()) {
-                    // Visit parents of target before visiting target
-                    visitParent(delta.movedTo);
-                    visited.add(delta);
-                    delta.movedTo.accept(this);
-                }
-                else {
-                    // Visit parents of removed before visiting remove
-                    visitParent(delta.removed);
-                    delta.removed.accept(this);
-                    listener.removeNode(delta.getPath());
-                    visited.add(delta);
-                }
-            }
-        }
-
-        @Override
-        public void visit(Moved delta) {
-            if (!visited.contains(delta)) {
-                delta.source.accept(this);
-                listener.moveNode(delta.getSourcePath(), delta.getPath());
-                visited.add(delta);
-                handleProperties(delta);
-                visitNodes(delta);
-            }
-        }
-
-        private void handleProperties(NodeDelta delta) {
-            Path path = delta.getPath();
-            for (Entry<String, JsonValue> property : delta.properties.entrySet()) {
-                String name = property.getKey();
-                JsonValue value = property.getValue();
-                if (value == null) {
-                    listener.removeProperty(path.concat(name));
-                }
-                else {
-                    listener.setProperty(path.concat(name), value);
-                }
-            }
-        }
-
-        private void visitNodes(NodeDelta delta) {
-            for (NodeDelta node : delta.childNodes()) {
-                node.accept(this);
-            }
-        }
-    }
-
-    /**
      * {@code NodeDelta} instances record changes to a node. {@code NodeDelta}'s
      * subclasses correspond to these changes:
      *
@@ -313,8 +135,6 @@ public class ChangeTree {
      *      added node.</li>
      * <li>{@link org.apache.jackrabbit.state.ChangeTree.Removed} represents a transiently
      *      removed node.</li>
-     * <li>{@link org.apache.jackrabbit.state.ChangeTree.Moved} represents a transiently
-     *      moved node.</li>
      * <li>{@link org.apache.jackrabbit.state.ChangeTree.Existing} represents a node which
      *      is otherwise touched. That is, which either has property modifications or a has a
      *      child node which is touched. </li>
@@ -346,35 +166,25 @@ public class ChangeTree {
         }
 
         /**
-         * @return persistent path to this node or {@code null} if this node is neither
-         * an {@link org.apache.jackrabbit.state.ChangeTree.Existing existing} node nor a
-         * {@link org.apache.jackrabbit.state.ChangeTree.Moved moved} node and this does
-         * not have a persistent path.
+         * @return persistent path to this node or {@code null} if this node is not
+         * an {@link org.apache.jackrabbit.state.ChangeTree.Existing existing} node.
          */
         public Path getPersistentPath() {
             return null;
         }
 
         /**
-         * @return persistent name to this node or {@code null} if this node is neither
-         * an {@link org.apache.jackrabbit.state.ChangeTree.Existing existing} node nor a
-         * {@link org.apache.jackrabbit.state.ChangeTree.Moved moved} node and this does
-         * not have a persistent name.
+         * @return {@code true} iff this node has been transiently removed.
          */
-        public String getPersistedName() {
-            return null;
-        }
+        public abstract boolean isRemoved();
 
         /**
-         * @return {@code true} iff this is node has been transiently removed. This is
-         * either the result of a remove or a move operation.
+         * @return {@code true} iff this node has been transiently added.
          */
-        public abstract boolean isRemoved();
+        public abstract boolean isAdded();
 
         /**
-         * @return {@code true} iff this node is moved. If at the same time {@link #isRemoved()}
-         * is {@code true}, this is the source node of the move operation. Otherwise this is
-         * the target node of the move operation.
+         * @return {@code true} idd this node has been transiently moved.
          */
         public abstract boolean isMoved();
 
@@ -383,21 +193,12 @@ public class ChangeTree {
          */
         public abstract boolean isTransient();
 
-        public abstract void accept(Visitor visitor);
-
         /**
-         * @return {@code true} iff this node has either changed child nodes or changed
-         * properties. 
+         * @return {@code true} iff this node has changes. A node has changes
+         * iff it either has changed properties or one of its child nodes has changes.
          */
         public boolean hasChanges() {
-            return !properties.isEmpty()
-                || Predicates.exists(childNodes.values(), new Predicate<NodeDelta>() {
-
-                @Override
-                public boolean evaluate(NodeDelta delta) {
-                    return delta.isTransient();
-                }
-            });
+            return !properties.isEmpty() || !childNodes.isEmpty();
         }
 
         /**
@@ -415,7 +216,7 @@ public class ChangeTree {
         public abstract NodeDelta getNode(String name);
 
         /**
-         * @return  Iterator of all added nodes and all nodes moved here.
+         * @return  Iterator of all added nodes
          */
         public Iterator<NodeDelta> getNodes() {
             return Iterators.filterIterator(childNodes().iterator(), new Predicate<NodeDelta>() {
@@ -468,7 +269,9 @@ public class ChangeTree {
                 throw new ItemExistsException(name);
             }
 
-            return addChild(new Added(this, name));
+            NodeDelta added = addChild(new Added(this, name));
+            notifyAdded(added);
+            return added;
         }
 
         /**
@@ -483,7 +286,9 @@ public class ChangeTree {
                 throw new ItemNotFoundException(name);
             }
 
-            return delta.remove();
+            NodeDelta removed = delta.remove();
+            notifyRemoved(removed);
+            return removed;
         }
 
         /**
@@ -511,7 +316,9 @@ public class ChangeTree {
                 throw new PathNotFoundException(destParentPath.toJcrPath());
             }
 
-            source.moveTo(destParentPath, destination.getName());
+            Path sourcePath = source.getPath();
+            NodeDelta moved = source.moveTo(destParentPath, destination.getName());
+            notifyMoved(sourcePath, moved);
         }
 
         /**
@@ -520,7 +327,7 @@ public class ChangeTree {
          * @param name
          * @param value
          */
-        public void setValue(String name, JsonValue value) {
+        public void setValue(String name, JsonValue value) {  // fixme: make this JsonAtom!?
             if (value == null && properties.containsKey(name) && properties.get(name) != null) {
                 properties.remove(name);
             }
@@ -528,14 +335,23 @@ public class ChangeTree {
                 properties.put(name, value);
                 touch();
             }
+            notifySetValue(this, name, value);
         }
 
         //------------------------------------------< internal >---
 
         void touch() { }
 
-        abstract NodeDelta remove() throws ItemNotFoundException;
-        abstract void moveTo(Path parentPath, String name);
+        NodeDelta remove() {
+            return parent.addChild(new Removed(parent, name));
+        }
+
+        NodeDelta moveTo(Path parentPath, String name) {
+            remove();
+            this.name = name;
+            NodeDelta parent = ChangeTree.this.getNode(parentPath);
+            return parent.addChild(this);
+        }
 
         final void clear() {
             childNodes.clear();
@@ -568,24 +384,64 @@ public class ChangeTree {
             }
             return delta;
         }
+        
+        private void notifyAdded(NodeDelta added) {
+            if (listener != null) {
+                listener.added(added);
+            }
+        }
+        
+        private void notifyRemoved(NodeDelta removed) {
+            if (listener != null) {
+                listener.removed(removed);
+            }
+        }
+        
+        private void notifyMoved(Path sourcePath, NodeDelta moved) {
+            if (listener != null) {
+                listener.moved(sourcePath, moved);
+            }
+        }
+        
+        private void notifySetValue(NodeDelta parent, String name, JsonValue value) {
+            if (listener != null) {
+                listener.setValue(parent, name, value);
+            }
+        }
+    }
+
+    //------------------------------------------< private/internal >---
+
+    /**
+     * @return A {@code Existing} instance for the given {@code parent} and {@code name}.
+     * Returns a previously allocated instance if not yet garbage collected.
+     * <em>Note:</em> returning fresh instances while previously allocated ones are still
+     * referenced in client code results in schizophrenia: same node multiple states.
+     */
+    private Existing existing(NodeDelta parent, String name, Path persistentPath) {
+        Existing e = existing.get(persistentPath);
+        if (e == null) {
+            e = new Existing(parent, name, persistentPath);
+            existing.put(persistentPath, e);
+        }
+        return e;
     }
 
     /**
      * Represents an existing node. That is, a node which exists on the persistence layer.
      */
-    public class Existing extends NodeDelta {
-        Existing(NodeDelta parent, String name) {
+    private class Existing extends NodeDelta {
+        private final Path persistentPath;
+        private boolean isMoved;
+
+        Existing(NodeDelta parent, String name, Path persistentPath) {
             super(parent, name);
+            this.persistentPath = persistentPath;
         }
 
         @Override
         public Path getPersistentPath() {
-            return getPath();
-        }
-
-        @Override
-        public String getPersistedName() {
-            return getName();
+            return persistentPath;
         }
 
         @Override
@@ -594,26 +450,27 @@ public class ChangeTree {
         }
 
         @Override
-        public boolean isMoved() {
+        public boolean isAdded() {
             return false;
         }
 
         @Override
-        public boolean isTransient() {
-            return false;
+        public boolean isMoved() {
+            return isMoved;
         }
 
         @Override
-        public void accept(Visitor visitor) {
-            visitor.visit(this);
+        public boolean isTransient() {
+            return !isMoved;
         }
 
         @Override
         public NodeDelta getNode(String name) {
             NodeDelta delta = getChild(name);
             if (delta == null) {
-                return nodeExists.evaluate(getPath().concat(name))
-                        ? existing(this, name)
+                Path path = persistentPath.concat(name);
+                return nodeExists.evaluate(path)
+                        ? existing(this, name, path)
                         : null;
             }
             else {
@@ -633,25 +490,19 @@ public class ChangeTree {
             return "Existing[" + getPath() + ']';
         }
 
-        @Override
-        NodeDelta remove() throws ItemNotFoundException {
-            return parent.addChild(new Removed(parent, name, this, null));
-        }
+        //------------------------------------------< internal >---
 
         @Override
-        void moveTo(Path parentPath, String name) {
-            Moved movedTo = new Moved(parent, name, this);
-            Removed removed = new Removed(parent, this.name, this, movedTo);
-            parent.addChild(removed);
-            NodeDelta parent = ChangeTree.this.getNode(parentPath);
-            parent.addChild(movedTo);
+        NodeDelta moveTo(Path parentPath, String name) {
+            isMoved = true;
+            return super.moveTo(parentPath, name);
         }
     }
 
     /**
      * Represents a transiently added node.
      */
-    public class Added extends NodeDelta {
+    private class Added extends NodeDelta {
         Added(NodeDelta parent, String name) {
             super(parent, name);
         }
@@ -662,80 +513,13 @@ public class ChangeTree {
         }
 
         @Override
-        public boolean isMoved() {
-            return false;
-        }
-
-        @Override
-        public boolean isTransient() {
+        public boolean isAdded() {
             return true;
         }
 
         @Override
-        public void accept(Visitor visitor) {
-            visitor.visit(this);
-        }
-
-        @Override
-        public NodeDelta getNode(String name) {
-            return getChild(name);
-        }
-
-        @Override
-        public String toString() {
-            return "Added[" + getPath() + ']';
-        }
-
-        @Override
-        NodeDelta remove() {
-            return parent.removeChild(name);
-        }
-
-        @Override
-        void moveTo(Path parentPath, String name) {
-            parent.removeChild(this.name);
-            NodeDelta destParent = ChangeTree.this.getNode(parentPath);
-            this.name = name;
-            destParent.addChild(this);
-        }
-    }
-
-    /**
-     * Represents a moved node.
-     */
-    public class Moved extends NodeDelta {
-        final NodeDelta source;
-
-        Moved(NodeDelta parent, String name, NodeDelta source) {
-            super(parent, name);
-            this.source = source;
-        }
-
-        /**
-         * @return  path to the source node of the move operation.
-         */
-        public Path getSourcePath() {
-            return source.getPath();
-        }
-
-        @Override
-        public Path getPersistentPath() {
-            return source.getPersistentPath();
-        }
-
-        @Override
-        public String getPersistedName() {
-            return source.getPersistedName();
-        }
-
-        @Override
-        public boolean isRemoved() {
-            return false;
-        }
-
-        @Override
         public boolean isMoved() {
-            return true;
+            return false;
         }
 
         @Override
@@ -744,54 +528,23 @@ public class ChangeTree {
         }
 
         @Override
-        public void accept(Visitor visitor) {
-            visitor.visit(this);
-        }
-
-        @Override
         public NodeDelta getNode(String name) {
-            NodeDelta delta = source.getNode(name);
-            if (delta == null) {
-                delta = getChild(name);
-            }
-            return delta;
+            NodeDelta delta = getChild(name);
+            return delta == null || delta.isRemoved() ? null : delta;
         }
 
         @Override
         public String toString() {
-            return "Moved[" + getPath() + "] from " + source;
-        }
-
-        @Override
-        NodeDelta remove() throws ItemNotFoundException {
-            // convert source from moved away to removed
-            if (source.parent.childNodes.remove(source.name) != null) {
-                source.parent.removeNode(source.name);
-            }
-
-            return parent.removeChild(name);
-        }
-
-        @Override
-        void moveTo(Path parentPath, String name) {
-            parent.removeChild(this.name);
-            NodeDelta destParent = ChangeTree.this.getNode(parentPath);
-            this.name = name;
-            destParent.addChild(this);
+            return "Added[" + getPath() + ']';
         }
     }
 
     /**
      * Represents a transiently removed node.
      */
-    public class Removed extends NodeDelta {
-        private final Existing removed;
-        private final Moved movedTo;
-
-        Removed(NodeDelta parent, String name, Existing removed, Moved movedTo) {
+    private class Removed extends NodeDelta {
+        Removed(NodeDelta parent, String name) {
             super(parent, name);
-            this.removed = removed;
-            this.movedTo = movedTo;
         }
 
         @Override
@@ -800,18 +553,18 @@ public class ChangeTree {
         }
 
         @Override
-        public boolean isMoved() {
-            return movedTo != null;
+        public boolean isAdded() {
+            return false;
         }
 
         @Override
-        public boolean isTransient() {
-            return true;
+        public boolean isMoved() {
+            return false;
         }
 
         @Override
-        public void accept(Visitor visitor) {
-            visitor.visit(this);
+        public boolean isTransient() {
+            return true;
         }
 
         @Override
@@ -840,23 +593,18 @@ public class ChangeTree {
         }
 
         @Override
-        NodeDelta remove() throws ItemNotFoundException {
+        NodeDelta remove() {
             throw new IllegalStateException("Removed");
         }
 
         @Override
-        void moveTo(Path parentPath, String name) {
+        NodeDelta moveTo(Path parentPath, String name) {
             throw new IllegalStateException("Removed");
         }
 
         @Override
         public String toString() {
-            if (isMoved()) {
-                return "Moved[" + getPath() + "] to " + movedTo.getPath();
-            }
-            else {
-                return "Removed[" + getPath() + ']';
-            }
+            return "Removed[" + getPath() + ']';
         }
     }
 

Modified: jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/NodeState.java
URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/NodeState.java?rev=1242885&r1=1242884&r2=1242885&view=diff
==============================================================================
--- jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/NodeState.java (original)
+++ jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/NodeState.java Fri Feb 10 18:54:21 2012
@@ -265,7 +265,7 @@ public class NodeState {
                 jsonObject = JsonObject.EMPTY;
             }
             else {
-                String json = getMicrokernel().getNodes(nodeDelta.getPersistentPath().toMkPath(), revision);
+                String json = getMicrokernel().getNodes(path.toMkPath(), revision);
                 jsonObject = FullJsonParser.parseObject(new UnescapingJsonTokenizer(json));
             }
         }

Modified: jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/TransientSpace.java
URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/TransientSpace.java?rev=1242885&r1=1242884&r2=1242885&view=diff
==============================================================================
--- jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/TransientSpace.java (original)
+++ jackrabbit/sandbox/jackrabbit-microkernel/src/main/java/org/apache/jackrabbit/state/TransientSpace.java Fri Feb 10 18:54:21 2012
@@ -38,6 +38,8 @@ import javax.jcr.RepositoryException;
 public class TransientSpace {
     private final MicroKernel microkernel;
     private final ChangeTree changeTree;
+    private final ChangeLog changeLog = new ChangeLog();
+
     private String revision;
 
     /**
@@ -51,12 +53,36 @@ public class TransientSpace {
         this.microkernel = microkernel;
         this.revision = revision;
 
-        changeTree = new ChangeTree(Path.create(workspace), new Predicate<Path>() {
+        Listener changeTreeListener = new Listener() {
+            @Override
+            public void added(NodeDelta nodeDelta) {
+                changeLog.addNode(nodeDelta.getPath());
+            }
+
+            @Override
+            public void removed(NodeDelta nodeDelta) {
+                changeLog.removeNode(nodeDelta.getPath());
+            }
+
+            @Override
+            public void moved(Path source, NodeDelta nodeDelta) {
+                changeLog.moveNode(source, nodeDelta.getPath());
+            }
+
+            @Override
+            public void setValue(NodeDelta parent, String name, JsonValue value) {
+                changeLog.setProperty(parent.getPath(), name, value == null ? null : value.asAtom());  // fixme: JsonValue should be JsonAtom
+            }
+        };
+
+        Predicate<Path> nodeExists = new Predicate<Path>() {
             @Override
             public boolean evaluate(Path path) {
                 return microkernel.nodeExists(path.toMkPath(), getRevision());
             }
-        });
+        };
+
+        changeTree = new ChangeTree(Path.create(workspace), changeTreeListener, nodeExists);
     }
 
     /**
@@ -76,50 +102,8 @@ public class TransientSpace {
      */
     public String save() throws RepositoryException {
         try {
-            final StringBuilder jsop = new StringBuilder();
-
-            changeTree.collectChanges(new Listener() {
-                @Override
-                public void addNode(Path path) {
-                    jsop.append("+\"")
-                            .append(path.toMkPath())
-                            .append("\":{}\n");
-                }
-
-                @Override
-                public void removeNode(Path path) {
-                    jsop.append("-\"")
-                            .append(path.toMkPath())
-                            .append("\"\n");
-                }
-
-                @Override
-                public void moveNode(Path sourcePath, Path destinationPath) {
-                    jsop.append(">\"")
-                            .append(sourcePath.toMkPath())
-                            .append("\":\"")
-                            .append(destinationPath.toMkPath())
-                            .append("\"\n");
-                }
-
-                @Override
-                public void setProperty(Path path, JsonValue value) {
-                    jsop.append("^\"")
-                            .append(path.toMkPath())
-                            .append("\":")
-                            .append(value.toJson())
-                            .append('\n');
-                }
-
-                @Override
-                public void removeProperty(Path path) {
-                    jsop.append("^\"")
-                            .append(path.toMkPath())
-                            .append("\":null\n");
-                }
-            });
-
-            revision = microkernel.commit("", jsop.toString(), revision, "");
+            revision = microkernel.commit("", changeLog.toJsop(), revision, "");
+            changeLog.clear();
             changeTree.clear();
             return revision;
         }

Added: jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceFuzzTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceFuzzTest.java?rev=1242885&view=auto
==============================================================================
--- jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceFuzzTest.java (added)
+++ jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceFuzzTest.java Fri Feb 10 18:54:21 2012
@@ -0,0 +1,456 @@
+/*
+ * 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.state;
+
+import org.apache.jackrabbit.Path;
+import org.apache.jackrabbit.json.JsonHandler;
+import org.apache.jackrabbit.json.JsonParser;
+import org.apache.jackrabbit.json.JsonTokenizer;
+import org.apache.jackrabbit.json.JsonValue.JsonAtom;
+import org.apache.jackrabbit.json.Token;
+import org.apache.jackrabbit.json.UnescapingJsonTokenizer;
+import org.apache.jackrabbit.mk.MicroKernelFactory;
+import org.apache.jackrabbit.mk.api.MicroKernel;
+import org.apache.jackrabbit.state.TransientSpaceFuzzTest.Operation.AddNode;
+import org.apache.jackrabbit.state.TransientSpaceFuzzTest.Operation.MoveNode;
+import org.apache.jackrabbit.state.TransientSpaceFuzzTest.Operation.RemoveNode;
+import org.apache.jackrabbit.state.TransientSpaceFuzzTest.Operation.Save;
+import org.apache.jackrabbit.state.TransientSpaceFuzzTest.Operation.SetProperty;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.ItemExistsException;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.RepositoryException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class TransientSpaceFuzzTest {
+    static final Logger log = LoggerFactory.getLogger(TransientSpaceFuzzTest.class);
+
+    private static final Path ROOT = Path.create("root");
+    private static final int OP_COUNT = 50000;
+
+    private final Random random;
+
+    private MicroKernel mk1;
+    private MicroKernel mk2;
+    private TransientSpace transientSpace;
+
+    @Parameters
+    public static List<Object[]> seeds() {
+        return Arrays.asList(new Object[][] {
+            {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9},
+            {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19},
+            {20}, {21}, {22}, {23}, {24}, {25}, {26}, {27}, {28}, {29},
+            {30}, {31}, {32}, {33}, {34}, {35}, {36}, {37}, {38}, {39},
+        });
+    }
+
+    public TransientSpaceFuzzTest(int seed) {
+        log.info("Seed = {}", seed);
+        random = new Random(seed);
+    }
+
+    @Before
+    public void setup() {
+        mk1 = MicroKernelFactory.getInstance("fs:target/repository-test/microkernel1;clean");
+        mk1.commit("", "+\"/root\":{}", mk1.getHeadRevision(), "");
+
+        mk2 = MicroKernelFactory.getInstance("fs:target/repository-test/microkernel2;clean");
+        mk2.commit("", "+\"/root\":{}", mk2.getHeadRevision(), "");
+        transientSpace = new TransientSpace("root", mk2, mk2.getHeadRevision());
+    }
+
+    @After
+    public void tearDown() {
+        mk2.dispose();
+        mk1.dispose();
+    }
+    
+    @Test
+    public void fuzzTest() throws Exception {
+        for (Operation op : operations(OP_COUNT)) {
+            log.info("{}", op);
+            op.apply(mk1);
+            op.apply(transientSpace);
+            if (op instanceof Save) {
+                checkEqual(ROOT);
+            }
+        }
+    }
+    
+    private Iterable<Operation> operations(final int count) {
+        return new Iterable<Operation>() {
+            int k = count;
+
+            @Override
+            public Iterator<Operation> iterator() {
+                return new Iterator<Operation>() {
+                    @Override
+                    public boolean hasNext() {
+                        return k-- > 0;
+                    }
+
+                    @Override
+                    public Operation next() {
+                        return createOperation();
+                    }
+
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+        };
+    }
+
+    abstract static class Operation {
+        abstract void apply(MicroKernel mk);
+        abstract void apply(TransientSpace transientSpace) throws RepositoryException;
+
+        static class AddNode extends Operation {
+            private final Path parent;
+            private final String name;
+
+            AddNode(Path parent, String name) {
+                this.parent = parent;
+                this.name = name;
+            }
+
+            @Override
+            void apply(MicroKernel mk) {
+                mk.commit("", "+\"" + parent.concat(name).toMkPath() + "\":{}",
+                        mk.getHeadRevision(), "");
+            }
+
+            @Override
+            void apply(TransientSpace transientSpace) throws ItemExistsException {
+                transientSpace.getNode(parent).addNode(name);
+            }
+
+            @Override
+            public String toString() {
+                return '+' + parent.concat(name).toMkPath() + ":{}";
+            }
+        }
+            
+        static class RemoveNode extends Operation {
+            private final Path path;
+
+            RemoveNode(Path path) {
+                this.path = path;
+            }
+
+            @Override
+            void apply(MicroKernel mk) {
+                mk.commit("", "-\"" + path.toMkPath() + '"', mk.getHeadRevision(), "");
+            }
+
+            @Override
+            void apply(TransientSpace transientSpace) throws ItemNotFoundException {
+                transientSpace.getNode(path.getParent()).removeNode(path.getName());
+            }
+
+            @Override
+            public String toString() {
+                return '-' + path.toMkPath();
+            }
+        }
+            
+        static class MoveNode extends Operation {
+            private final Path source;
+            private final Path destination;
+
+            MoveNode(Path source, Path destParent, String destName) {
+                this.source = source;
+                destination = destParent.concat(destName);
+            }
+
+            @Override
+            void apply(MicroKernel mk) {
+                mk.commit("", ">\"" + source.toMkPath() + "\":\"" + destination.toMkPath() + '"',
+                        mk.getHeadRevision(), "");
+            }
+
+            @Override
+            void apply(TransientSpace transientSpace) throws RepositoryException {
+                transientSpace.getNode(source.getParent()).moveNode(source.getName(), destination);
+            }
+
+            @Override
+            public String toString() {
+                return '>' + source.toMkPath() + ':' + destination.toMkPath();
+            }
+        }
+            
+        static class SetProperty extends Operation {
+            private final Path parent;
+            private final String name;
+            private final JsonAtom value;
+
+            SetProperty(Path parent, String name, JsonAtom value) {
+                this.parent = parent;
+                this.name = name;
+                this.value = value;
+            }
+
+            @Override
+            void apply(MicroKernel mk) {
+                mk.commit("", "^\"" + parent.concat(name).toMkPath() + "\":" + value.toJson(),
+                        mk.getHeadRevision(), "");
+            }
+
+            @Override
+            void apply(TransientSpace transientSpace) throws RepositoryException {
+                transientSpace.getNode(parent).setValue(name, value);
+            }
+
+            @Override
+            public String toString() {
+                return '^' + parent.concat(name).toMkPath() + ':' + value.toJson();
+            }
+        }
+
+        static class Save extends Operation {
+            @Override
+            void apply(MicroKernel mk) {
+                // empty
+            }
+
+            @Override
+            void apply(TransientSpace transientSpace) throws RepositoryException {
+                transientSpace.save();
+            }
+
+            @Override
+            public String toString() {
+                return "save";
+            }
+        }
+    }
+
+    private Operation createOperation() {
+        Operation op;
+        do {
+            switch (random.nextInt(9)) {
+                case 0:
+                case 1:
+                case 2:
+                    op = createAddNode();
+                    break;
+                case 3:
+                    op = createRemoveNode();
+                    break;
+                case 4:
+                    op = createMoveNode();
+                    break;
+                case 5:
+                    op = createAddProperty();
+                    break;
+                case 6:
+                    op = createSetProperty();
+                    break;
+                case 7:
+                    op = createRemoveProperty();
+                    break;
+                case 8:
+                    op = new Save();
+                    break;
+                default:
+                    throw new IllegalStateException();
+            }
+        } while (op == null);
+        return op;
+    }
+
+    private Operation createAddNode() {
+        Path parent = chooseNodePath();
+        String name = createNodeName();
+        return new AddNode(parent, name);
+    }
+
+    private Operation createRemoveNode() {
+        Path path = chooseNodePath();
+        return ROOT.equals(path) ? null : new RemoveNode(path);
+    }
+
+    private Operation createMoveNode() {
+        Path source = chooseNodePath();
+        Path destParent = chooseNodePath();
+        String destName = createNodeName();
+        return ROOT.equals(source) || destParent.toJcrPath().startsWith(source.toJcrPath())
+            ? null 
+            : new MoveNode(source, destParent, destName);
+    }
+
+    private Operation createAddProperty() {
+        Path parent = chooseNodePath();
+        String name = createPropertyName();
+        JsonAtom value = createValue();
+        return new SetProperty(parent, name, value);
+    }
+
+    private Operation createSetProperty() {
+        Path path = choosePropertyPath();
+        if (path == null) {
+            return null;
+        }
+        JsonAtom value = createValue();
+        return new SetProperty(path.getParent(), path.getName(), value);
+    }
+
+    private Operation createRemoveProperty() {
+        Path path = choosePropertyPath();
+        if (path == null) {
+            return null;
+        }
+        return new SetProperty(path.getParent(), path.getName(), JsonAtom.NULL);
+    }
+
+    private int counter;
+
+    private String createNodeName() {
+        return "N" + counter++;
+    }
+
+    private String createPropertyName() {
+        return "P" + counter++;
+    }
+
+    private Path chooseNodePath() {
+        Path path = ROOT;
+
+        Path next;
+        while ((next = chooseNode(path)) != null) {
+            path = next;
+        }
+
+        return path;
+    }
+
+    private Path choosePropertyPath() {
+        return chooseProperty(chooseNodePath());
+    }
+
+    private Path chooseNode(final Path parent) {
+        final List<Path> nodes = new ArrayList<Path>();
+        String json = mk1.getNodes(parent.toMkPath(), mk1.getHeadRevision(), 0, 0, -1);
+        new JsonParser(new JsonHandler(){
+            @Override
+            public void object(JsonParser parser, Token key, JsonTokenizer tokenizer) {
+                new JsonParser(JsonHandler.INSTANCE).parseObject(tokenizer);
+                nodes.add(parent.concat(key.text()));
+            }
+        }).parseObject(new UnescapingJsonTokenizer(json));
+
+        int k = random.nextInt(nodes.size() + 1);
+        if (k < nodes.size()) {
+            return nodes.get(k);
+        }
+        else {
+            return null;
+        }
+    }
+
+    private Path chooseProperty(final Path parent) {
+        final List<Path> properties = new ArrayList<Path>();
+        String json = mk1.getNodes(parent.toMkPath(), mk1.getHeadRevision(), 0, 0, -1);
+        new JsonParser(new JsonHandler(){
+            @Override
+            public void atom(Token key, Token value) {
+                if (!key.text().startsWith(":")) {
+                    properties.add(parent.concat(key.text()));
+                }
+            }
+        }).parseObject(new UnescapingJsonTokenizer(json));
+
+        int k = random.nextInt(properties.size() + 1);
+        if (k < properties.size()) {
+            return properties.get(k);
+        }
+        else {
+            return null;
+        }
+    }
+
+    private JsonAtom createValue() {
+        return JsonAtom.string("V" + counter++);
+    }
+
+    private void checkEqual(Path path) {
+        assertTrue(path.toString(), mk1.nodeExists(path.toMkPath(), mk1.getHeadRevision()));
+        assertTrue(path.toString(), mk2.nodeExists(path.toMkPath(), mk2.getHeadRevision()));
+
+
+        final Set<String> nodeNames1 = new HashSet<String>();
+        final Map<String, JsonAtom> properties1 = new HashMap<String, JsonAtom>();
+        collectItems(mk1, path, nodeNames1, properties1);
+
+        final Set<String> nodeNames2 = new HashSet<String>();
+        final Map<String, JsonAtom> properties2 = new HashMap<String, JsonAtom>();
+        collectItems(mk2, path, nodeNames2, properties2);
+
+        assertEquals(path.toString(), nodeNames1, nodeNames2);
+        assertEquals(path.toString(), properties1, properties2);
+
+        for (String name : nodeNames1) {
+            checkEqual(path.concat(name));
+        }
+    }
+
+    private static void collectItems(MicroKernel microKernel, Path path, final Set<String> nodeNames,
+            final Map<String, JsonAtom> properties) {
+
+        String json = microKernel.getNodes(path.toMkPath(), microKernel.getHeadRevision(), 0, 0, -1);
+        new JsonParser(new JsonHandler(){
+            @Override
+            public void object(JsonParser parser, Token key, JsonTokenizer tokenizer) {
+                new JsonParser(JsonHandler.INSTANCE).parseObject(tokenizer);
+                nodeNames.add(key.text());
+            }
+
+            @Override
+            public void atom(Token key, Token value) {
+                super.atom(key, value);
+                properties.put(key.text(), new JsonAtom(value));
+            }
+        }).parseObject(new UnescapingJsonTokenizer(json));
+    }
+
+}

Modified: jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceTest.java?rev=1242885&r1=1242884&r2=1242885&view=diff
==============================================================================
--- jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceTest.java (original)
+++ jackrabbit/sandbox/jackrabbit-microkernel/src/test/java/org/apache/jackrabbit/state/TransientSpaceTest.java Fri Feb 10 18:54:21 2012
@@ -11,18 +11,12 @@ import org.apache.jackrabbit.mk.MicroKer
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.mk.api.MicroKernelException;
 import org.apache.jackrabbit.mk.store.NotFoundException;
-import org.apache.jackrabbit.state.ChangeTree.Added;
-import org.apache.jackrabbit.state.ChangeTree.Existing;
-import org.apache.jackrabbit.state.ChangeTree.Moved;
 import org.apache.jackrabbit.state.ChangeTree.NodeDelta;
-import org.apache.jackrabbit.state.ChangeTree.Removed;
-import org.apache.jackrabbit.state.ChangeTree.Visitor;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.internal.matchers.TypeSafeMatcher;
 
@@ -150,7 +144,7 @@ public class TransientSpaceTest {
         added.removeNode("added2");
     }
 
-    @Test @Ignore("WIP")
+    @Test
     public void removeAndAddAgain() throws RepositoryException {
         NodeDelta root = transientSpace.getNode(ROOT);
         root.addNode("a").addNode("b");
@@ -175,7 +169,7 @@ public class TransientSpaceTest {
         assertFalse(root.getNode("a").hasNode("b"));
     }
 
-    @Test @Ignore("WIP")
+    @Test
     public void moveAndAddAgain() throws RepositoryException {
         NodeDelta root = transientSpace.getNode(ROOT);
         root.addNode("a").addNode("b");
@@ -1123,46 +1117,4 @@ public class TransientSpaceTest {
         }
     }
 
-    /* Debugging */
-    @SuppressWarnings("UseOfSystemOutOrSystemErr")
-    private static void dump(NodeDelta delta) {
-        delta.accept(new Visitor() {
-            private String ident = "";
-            
-
-            @Override
-            public void visit(Existing delta) {
-                System.out.println(ident + delta.getName() + ":E");
-                visitChildren(delta);
-            }
-
-            @Override
-            public void visit(Added delta) {
-                System.out.println(ident + delta.getName() + ":A");
-                visitChildren(delta);
-            }
-
-            @Override
-            public void visit(Removed delta) {
-                System.out.println(ident + delta.getName() + (delta.isMoved() ? ":M-" : ":R"));
-                visitChildren(delta);
-            }
-
-            @Override
-            public void visit(Moved delta) {
-                System.out.println(ident + delta.getName() + ":M+");
-                visitChildren(delta.source);
-                visitChildren(delta);
-            }
-
-            private void visitChildren(NodeDelta delta) {
-                ident = ident + "  ";
-                for (NodeDelta child : delta.childNodes()) {
-                    child.accept(this);
-                }
-                ident = ident.substring(0, ident.length() - 2);
-            }
-            
-        });
-    }
 }