You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by dp...@apache.org on 2008/04/09 15:33:47 UTC

svn commit: r646336 - in /jackrabbit/trunk/jackrabbit-core/src: main/java/org/apache/jackrabbit/core/ main/java/org/apache/jackrabbit/core/state/ test/java/org/apache/jackrabbit/core/

Author: dpfister
Date: Wed Apr  9 06:33:46 2008
New Revision: 646336

URL: http://svn.apache.org/viewvc?rev=646336&view=rev
Log:
JCR-1104 - JSR 283 support
- shareble nodes (work in progress)
- improve share-cycle detection

Modified:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java Wed Apr  9 06:33:46 2008
@@ -310,9 +310,8 @@
         // 4. detect share cycle
         NodeId srcId = srcState.getNodeId();
         NodeId destParentId = destParentState.getNodeId();
-        if (destParentId.equals(srcId) ||
-                hierMgr.isAncestor(srcId, destParentId)) {
-            String msg = "This would create a share cycle.";
+        if (destParentId.equals(srcId) || hierMgr.isAncestor(srcId, destParentId)) {
+            String msg = "Share cycle detected.";
             log.debug(msg);
             throw new RepositoryException(msg);
         }
@@ -546,6 +545,14 @@
             // subscript in name element
             String msg = safeGetJCRPath(destPath)
                     + ": invalid destination path (subscript in name element is not allowed)";
+            log.debug(msg);
+            throw new RepositoryException(msg);
+        }
+
+        HierarchyManagerImpl hierMgr = (HierarchyManagerImpl) this.hierMgr;
+        if (hierMgr.isShareAncestor(target.getNodeId(), destParent.getNodeId())) {
+            String msg = safeGetJCRPath(destPath)
+                    + ": invalid destination path (share cycle detected)";
             log.debug(msg);
             throw new RepositoryException(msg);
         }

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java Wed Apr  9 06:33:46 2008
@@ -93,18 +93,18 @@
      * @throws RepositoryException
      */
     Name getName(ItemId id) throws ItemNotFoundException, RepositoryException;
-    
+
     /**
      * Returns the name of the specified item, with the given parent id. If the
      * given item is not shareable, this is identical to {@link #getName(ItemId)}.
-     * 
+     *
      * @param id node id
      * @param parentId parent node id
      * @return name
      * @throws ItemNotFoundException
      * @throws RepositoryException
      */
-    Name getName(NodeId id, NodeId parentId) 
+    Name getName(NodeId id, NodeId parentId)
             throws ItemNotFoundException, RepositoryException;
 
     /**
@@ -154,5 +154,52 @@
      * @throws RepositoryException   if another error occurs
      */
     boolean isAncestor(NodeId nodeId, ItemId itemId)
+            throws ItemNotFoundException, RepositoryException;
+
+    //------------------------------------------- operation with shareable nodes
+
+    /**
+     * Determines whether the node with the specified <code>ancestor</code>
+     * is a share ancestor of the item denoted by the given <code>descendant</code>.
+     * This is <code>true</code> for two nodes <code>A</code>, <code>B</code>
+     * if either:
+     * <ul>
+     * <li><code>A</code> is a (proper) ancestor of <code>B</code></li>
+     * <li>there is a non-empty sequence of nodes <code>N<sub>1</sub></code>,...
+     * ,<code>N<sub>k</sub></code> such that <code>A</code>=
+     * <code>N<sub>1</sub></code> and <code>B</code>=<code>N<sub>k</sub></code>
+     * and <code>N<sub>i</sub></code> is the parent or a share-parent of
+     * <code>N<sub>i+1</sub></code> (for every <code>i</code> in <code>1</code>
+     * ...<code>k-1</code>.</li>
+     * </ul>
+     *
+     * @param nodeId node id
+     * @param itemId item id
+     * @return <code>true</code> if the node denoted by <code>ancestor</code>
+     *         is a share ancestor of the item denoted by <code>descendant</code>,
+     *         <code>false</code> otherwise
+     * @throws ItemNotFoundException if any of the specified id's does not
+     *                               denote an existing item.
+     * @throws RepositoryException   if another error occurs
+     */
+    boolean isShareAncestor(NodeId ancestor, NodeId descendant)
+            throws ItemNotFoundException, RepositoryException;
+
+    /**
+     * Returns the depth of the specified share-descendant relative to the given
+     * share-ancestor. If <code>ancestor</code> and <code>descendant</code>
+     * denote the same item, <code>0</code> is returned. If <code>ancestor</code>
+     * does not denote an share-ancestor <code>-1</code> is returned.
+     *
+     * @param ancestor ancestor id
+     * @param descendant descendant id
+     * @return the relative depth; <code>-1</code> if <code>ancestor</code> does
+     *         not denote a share-ancestor of the item denoted by <code>descendant</code>
+     *         (or itself).
+     * @throws ItemNotFoundException if either of the specified id's does not
+     *                               denote an existing item.
+     * @throws RepositoryException   if another error occurs
+     */
+    int getShareRelativeDepth(NodeId ancestorId, ItemId descendantId)
             throws ItemNotFoundException, RepositoryException;
 }

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java Wed Apr  9 06:33:46 2008
@@ -16,6 +16,11 @@
  */
 package org.apache.jackrabbit.core;
 
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
 import org.apache.jackrabbit.core.state.ItemState;
 import org.apache.jackrabbit.core.state.ItemStateException;
 import org.apache.jackrabbit.core.state.ItemStateManager;
@@ -216,6 +221,33 @@
     }
 
     /**
+     * Return all parents of a node. A shareable node has possibly more than
+     * one parent.
+     *
+     * @param state item state
+     * @return set of parent <code>NodeId</code>s. If state has no parent,
+     *         array has length <code>0</code>.
+     */
+    protected Set getParentIds(ItemState state) {
+        if (state.isNode()) {
+            // if this is a node, quickly check whether it is shareable and
+            // whether it contains more than one parent
+            NodeState ns = (NodeState) state;
+            Set s = ns.getSharedSet();
+            if (s.size() > 1) {
+                return s;
+            }
+        }
+        NodeId parentId = getParentId(state);
+        if (parentId != null) {
+            LinkedHashSet s = new LinkedHashSet();
+            s.add(parentId);
+            return s;
+        }
+        return Collections.EMPTY_SET;
+    }
+
+    /**
      * Returns the <code>ChildNodeEntry</code> of <code>parent</code> with the
      * specified <code>uuid</code> or <code>null</code> if there's no such entry.
      * <p/>
@@ -453,7 +485,7 @@
             throws ItemNotFoundException, RepositoryException {
 
         NodeState parentState;
-        
+
         try {
             parentState = (NodeState) getItemState(parentId);
         } catch (NoSuchItemStateException nsis) {
@@ -475,7 +507,7 @@
         }
         return entry.getName();
     }
-    
+
     /**
      * {@inheritDoc}
      */
@@ -574,5 +606,93 @@
             throw new RepositoryException(msg, ise);
         }
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isShareAncestor(NodeId ancestor, NodeId descendant)
+            throws ItemNotFoundException, RepositoryException {
+        if (ancestor.equals(descendant)) {
+            // can't be ancestor of self
+            return false;
+        }
+        try {
+            ItemState state = getItemState(descendant);
+            Set parentIds = getParentIds(state);
+            while (parentIds.size() > 0) {
+                if (parentIds.contains(ancestor)) {
+                    return true;
+                }
+                Set grandparentIds = new LinkedHashSet();
+                Iterator iter = parentIds.iterator();
+                while (iter.hasNext()) {
+                    NodeId parentId = (NodeId) iter.next();
+                    grandparentIds.addAll(getParentIds(getItemState(parentId)));
+                }
+                parentIds = grandparentIds;
+            }
+            // not an ancestor
+            return false;
+        } catch (NoSuchItemStateException nsise) {
+            String msg = "failed to determine degree of relationship of "
+                    + ancestor + " and " + descendant;
+            log.debug(msg);
+            throw new ItemNotFoundException(msg, nsise);
+        } catch (ItemStateException ise) {
+            String msg = "failed to determine degree of relationship of "
+                    + ancestor + " and " + descendant;
+            log.debug(msg);
+            throw new RepositoryException(msg, ise);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int getShareRelativeDepth(NodeId ancestor, ItemId descendant)
+            throws ItemNotFoundException, RepositoryException {
+
+        if (ancestor.equals(descendant)) {
+            return 0;
+        }
+        int depth = 1;
+        try {
+            ItemState state = getItemState(descendant);
+            if (state.hasOverlayedState()) {
+                state = state.getOverlayedState();
+            }
+            Set parentIds = getParentIds(state);
+            while (parentIds.size() > 0) {
+                if (parentIds.contains(ancestor)) {
+                    return depth;
+                }
+                depth++;
+                Set grandparentIds = new LinkedHashSet();
+                Iterator iter = parentIds.iterator();
+                while (iter.hasNext()) {
+                    NodeId parentId = (NodeId) iter.next();
+                    state = getItemState(parentId);
+                    if (state.hasOverlayedState()) {
+                        state = state.getOverlayedState();
+                    }
+                    grandparentIds.addAll(getParentIds(state));
+                }
+                parentIds = grandparentIds;
+            }
+            // not an ancestor
+            return -1;
+        } catch (NoSuchItemStateException nsise) {
+            String msg = "failed to determine degree of relationship of "
+                    + ancestor + " and " + descendant;
+            log.debug(msg);
+            throw new ItemNotFoundException(msg, nsise);
+        } catch (ItemStateException ise) {
+            String msg = "failed to determine degree of relationship of "
+                    + ancestor + " and " + descendant;
+            log.debug(msg);
+            throw new RepositoryException(msg, ise);
+        }
+    }
+
 }
 

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java Wed Apr  9 06:33:46 2008
@@ -434,7 +434,7 @@
         node = (NodeImpl) getItem(id);
         if (!node.getParentId().equals(parentId)) {
             // verify that parent actually appears in the shared set
-            if (!node.hasSharedParent(parentId)) {
+            if (!node.hasShareParent(parentId)) {
                 String msg = "Node with id '" + id
                         + "' does not have shared parent with id: " + parentId;
                 throw new ItemNotFoundException(msg);

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java Wed Apr  9 06:33:46 2008
@@ -2043,7 +2043,7 @@
 
         // (5) do clone operation
         NodeId parentId = getNodeId();
-        src.addShare(parentId);
+        src.addShareParent(parentId);
 
         // (6) modify the state of 'this', i.e. the parent node
         NodeId srcId = src.getNodeId();
@@ -3181,18 +3181,19 @@
      *         <code>false</code> otherwise.
      * @see NodeState#isShareable()
      */
-    protected boolean isShareable() {
+    boolean isShareable() {
        return ((NodeState) state).isShareable();
     }
 
     /**
      * Helper method, returning the parent id this node is attached to. If this
      * node is shareable, it returns the primary parent id (which remains
-     * fixed). Otherwise returns the underlying state's parent id.
+     * fixed since shareable nodes are not moveable). Otherwise returns the
+     * underlying state's parent id.
      *
      * @return parent id
      */
-    protected NodeId getParentId() {
+    NodeId getParentId() {
         if (primaryParentId != null) {
             return primaryParentId;
         }
@@ -3201,27 +3202,27 @@
 
     /**
      * Helper method, returning a flag indicating whether this node has
-     * the given shared parent.
+     * the given share-parent.
      *
      * @param parentId parent id
      * @return <code>true</code> if the node has the given shared parent;
      *         <code>false</code> otherwise.
      */
-    protected boolean hasSharedParent(NodeId parentId) {
+    boolean hasShareParent(NodeId parentId) {
         return ((NodeState) state).containsShare(parentId);
     }
 
     /**
-     * Add a parent to the shared set. This method checks first, whether:
+     * Add a share-parent to this node. This method checks, whether:
      * <ul>
      * <li>this node is shareable</li>
-     * <li>adding this parent would create a share cycle</li>
-     * <li>whether this parent is already contained in the shared set</li>
+     * <li>adding the given would create a share cycle</li>
+     * <li>the given parent is already a share-parent</li>
      * </ul>
      * @param parentId parent to add to the shared set
      * @throws RepositoryException if an error occurs
      */
-    protected void addShare(NodeId parentId) throws RepositoryException {
+    void addShareParent(NodeId parentId) throws RepositoryException {
         // verify that we're shareable
         if (!isShareable()) {
             String msg = "Node at " + safeGetJCRPath() + " is not shareable.";
@@ -3293,8 +3294,10 @@
     /**
      * Invoked when another node in the same shared set has replaced the
      * node state.
+     *
+     * @param state state that is now stored as <code>NodeImpl</code>'s state
      */
-    protected void stateReplaced(NodeState state) {
+    void stateReplaced(NodeState state) {
         this.state = state;
     }
 

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java Wed Apr  9 06:33:46 2008
@@ -1556,6 +1556,13 @@
             log.debug(msg);
             throw new RepositoryException(msg, e);
         }
+
+        if (hierMgr.isShareAncestor(targetNode.getNodeId(), destParentNode.getNodeId())) {
+            String msg = destAbsPath + ": invalid destination path (share cycle detected)";
+            log.debug(msg);
+            throw new RepositoryException(msg);
+        }
+
         int ind = destName.getIndex();
         if (ind > 0) {
             // subscript in name element
@@ -1645,7 +1652,7 @@
                 log.debug(msg);
                 throw new UnsupportedRepositoryOperationException(msg);
             }
-            
+
             // do move:
             // 1. remove child node entry from old parent
             NodeState srcParentState =

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java Wed Apr  9 06:33:46 2008
@@ -19,6 +19,7 @@
 import org.apache.commons.collections.iterators.IteratorChain;
 import org.apache.jackrabbit.core.CachingHierarchyManager;
 import org.apache.jackrabbit.core.HierarchyManager;
+import org.apache.jackrabbit.core.HierarchyManagerImpl;
 import org.apache.jackrabbit.core.ItemId;
 import org.apache.jackrabbit.core.NodeId;
 import org.apache.jackrabbit.core.PropertyId;
@@ -377,7 +378,7 @@
      * Returns an iterator over those transient item state instances that are
      * direct or indirect descendants of the item state with the given
      * <code>parentId</code>. The transient item state instance with the given
-     * <code>parentId</code> itself (if there is such) will not be included.
+     * <code>parentId</code> itself (if there is such)                                                                            not be included.
      * <p/>
      * The instances are returned in depth-first tree traversal order.
      *
@@ -407,7 +408,7 @@
                 // determine relative depth: > 0 means it's a descendant
                 int depth;
                 try {
-                    depth = hierMgr.getRelativeDepth(parentId, state.getId());
+                    depth = hierMgr.getShareRelativeDepth(parentId, state.getId());
                 } catch (ItemNotFoundException infe) {
                     /**
                      * one of the parents of the specified item has been
@@ -520,7 +521,8 @@
             while (iter.hasNext()) {
                 ItemState state = (ItemState) iter.next();
                 // determine relative depth: > 0 means it's a descendant
-                int depth = zombieHierMgr.getRelativeDepth(parentId, state.getId());
+                //int depth = zombieHierMgr.getRelativeDepth(parentId, state.getId());
+                int depth = zombieHierMgr.getShareRelativeDepth(parentId, state.getId());
                 if (depth < 1) {
                     // not a descendant
                     continue;

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java?rev=646336&r1=646335&r2=646336&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java Wed Apr  9 06:33:46 2008
@@ -298,14 +298,14 @@
         a2.getNode("b2").remove();
         a2.save();
 
-        // verify shareable set contains one element only
+        // verify shared set contains one element only
         Node[] shared = getSharedSet(b1);
         assertEquals(1, shared.length);
 
         // restore version
         a2.restore(v, false);
 
-        // verify shareable set contains two elements again
+        // verify shared set contains again two elements
         shared = getSharedSet(b1);
         assertEquals(2, shared.length);
     }
@@ -496,18 +496,27 @@
         workspace.copy(s.getPath(), testRootNode.getPath() + "/d");
 
         // verify source contains shared set with 2 entries
-        Node[] shared = getSharedSet(b1);
-        assertEquals(2, shared.length);
+        Node[] shared1 = getSharedSet(b1);
+        assertEquals(2, shared1.length);
 
         // verify destination contains shared set with 2 entries
-        shared = getSharedSet(testRootNode.getNode("d/a1/b1"));
-        assertEquals(2, shared.length);
+        Node[] shared2 = getSharedSet(testRootNode.getNode("d/a1/b1"));
+        assertEquals(2, shared2.length);
+
+        // verify elements in source shared set and destination shared set
+        // don't have the same UUID
+        String srcUUID = shared1[0].getUUID();
+        String destUUID = shared2[0].getUUID();
+        assertFalse(
+                "Source and destination of a copy must not have the same UUID",
+                srcUUID.equals(destUUID));
     }
 
     /**
-     * Verify that a share cycle is detected (6.13.13).
+     * Verify that a share cycle is detected (6.13.13) when a shareable node
+     * is cloned.
      */
-    public void testShareCycle() throws Exception {
+    public void testDetectShareCycleOnClone() throws Exception {
         // setup parent nodes and first child
         Node a1 = testRootNode.addNode("a1");
         Node b1 = a1.addNode("b1");
@@ -523,7 +532,79 @@
             // clone underneath b1: this must fail
             workspace.clone(workspace.getName(), b1.getPath(),
                     b1.getPath() + "/c", false);
-            fail("Cloning should create a share cycle.");
+            fail("Share cycle not detected on clone.");
+        } catch (RepositoryException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Verify that a share cycle is detected (6.13.13) when a node is moved.
+     */
+    public void testDetectShareCycleOnMove() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Workspace workspace = b1.getSession().getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node
+        Node c = b1.addNode("c");
+        b1.save();
+
+        Node[] shared = getSharedSet(b1);
+        assertEquals(2, shared.length);
+
+        // move node
+        try {
+            workspace.move(testRootNode.getPath() + "/a2", c.getPath() + "/d");
+            fail("Share cycle not detected on move.");
+        } catch (RepositoryException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Verify that a share cycle is detected (6.13.13) when a node is
+     * transiently moved.
+     */
+    public void testDetectShareCycleOnTransientMove() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Session session = b1.getSession();
+        Workspace workspace = session.getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node
+        Node c = b1.addNode("c");
+        b1.save();
+
+        Node[] shared = getSharedSet(b1);
+        assertEquals(2, shared.length);
+
+        // move node
+        try {
+            session.move(testRootNode.getPath() + "/a2", c.getPath());
+            fail("Share cycle not detected on transient move.");
         } catch (RepositoryException e) {
             // expected
         }
@@ -953,9 +1034,10 @@
     }
 
     /**
-     * Restore a shareable node and remove an existing shareable node (6.13.19)
-     * In this case the particular shared node is removed but its descendants
-     * continue to exist below the remaining members of the shared set.
+     * Restore a shareable node that automatically removes an existing shareable
+     * node (6.13.19). In this case the particular shared node is removed but
+     * its descendants continue to exist below the remaining members of the
+     * shared set.
      */
     public void testRestoreRemoveExisting() throws Exception {
         // setup parent nodes and first child
@@ -1037,42 +1119,6 @@
     }
 
     /**
-     * Clone a mix:shareable node to the same workspace multiple times, remove
-     * all parents and save. Exposes an error that occurred when having more
-     * than two members in a shared set and parents were removed in the same
-     * order they were created.
-     */
-    public void testCloneMultipleTimes() throws Exception {
-        final int count = 10;
-        Node[] parents = new Node[count];
-
-        // setup parent nodes and first child
-        for (int i = 0; i < parents.length; i++) {
-            parents[i] = testRootNode.addNode("a" + (i + 1));
-        }
-        Node b = parents[0].addNode("b");
-        testRootNode.save();
-
-        // add mixin
-        b.addMixin("mix:shareable");
-        b.save();
-
-        Workspace workspace = b.getSession().getWorkspace();
-
-        // clone to all other nodes
-        for (int i = 1; i < parents.length; i++) {
-            workspace.clone(workspace.getName(), b.getPath(),
-                    parents[i].getPath() + "/b", false);
-        }
-
-        // remove all parents and save
-        for (int i = 0; i < parents.length; i++) {
-            parents[i].remove();
-        }
-        testRootNode.save();
-    }
-
-    /**
      * Verify that Node.isSame returns <code>true</code> for shareable nodes
      * in the same shared set (6.13.21)
      */
@@ -1244,6 +1290,188 @@
         } catch (UnsupportedRepositoryOperationException e) {
             // expected
         }
+    }
+
+    //----------------------------------------------------- implementation tests
+
+    /**
+     * Verify that invoking save() on a share-ancestor will save changes in
+     * all share-descendants.
+     */
+    public void testRemoveDescendantAndSave() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Session session = b1.getSession();
+        Workspace workspace = b1.getSession().getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node c to b1
+        Node c = b1.addNode("c");
+        b1.save();
+
+        // remove child node c
+        c.remove();
+
+        // save a2 (having path /testroot/a2): this should save c as well
+        // since one of the paths to c is /testroot/a2/b2/c
+        a2.save();
+        assertFalse("Saving share-ancestor should save share-descendants",
+                session.hasPendingChanges());
+    }
+
+    /**
+     * Verify that invoking save() on a share-ancestor will save changes in
+     * all share-descendants.
+     */
+    public void testRemoveDescendantAndRemoveShareAndSave() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Session session = b1.getSession();
+        Workspace workspace = b1.getSession().getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node c to b1
+        Node c = b1.addNode("c");
+        b1.save();
+
+        // remove child node c
+        c.remove();
+
+        // remove share b2 from a2
+        ((NodeImpl) a2.getNode("b2")).removeShare();
+
+        // save a2 (having path /testroot/a2): this should save c as well
+        // since one of the paths to c was /testroot/a2/b2/c
+        a2.save();
+        assertFalse("Saving share-ancestor should save share-descendants",
+                session.hasPendingChanges());
+    }
+
+    /**
+     * Verify that invoking save() on a share-ancestor will save changes in
+     * all share-descendants.
+     */
+    public void testModifyDescendantAndSave() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Workspace workspace = b1.getSession().getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node c to b1
+        Node c = b1.addNode("c");
+        b1.save();
+
+        // add child d to c, this modifies c
+        c.addNode("d");
+
+        // save a2 (having path /testroot/a2): this should save c as well
+        // since one of the paths to c is /testroot/a2/b2/c
+        a2.save();
+        assertFalse("Saving share-ancestor should save share-descendants",
+                c.isModified());
+    }
+
+    /**
+     * Verify that invoking save() on a share-ancestor will save changes in
+     * all share-descendants.
+     */
+    public void testModifyDescendantAndRemoveShareAndSave() throws Exception {
+        // setup parent nodes and first child
+        Node a1 = testRootNode.addNode("a1");
+        Node a2 = testRootNode.addNode("a2");
+        Node b1 = a1.addNode("b1");
+        testRootNode.save();
+
+        // add mixin
+        b1.addMixin("mix:shareable");
+        b1.save();
+
+        // clone
+        Workspace workspace = b1.getSession().getWorkspace();
+        workspace.clone(workspace.getName(), b1.getPath(),
+                a2.getPath() + "/b2", false);
+
+        // add child node c to b1
+        Node c = b1.addNode("c");
+        b1.save();
+
+        // add child d to c, this modifies c
+        c.addNode("d");
+
+        // remove share b2 from a2
+        ((NodeImpl) a2.getNode("b2")).removeShare();
+
+        // save a2 (having path /testroot/a2): this should save c as well
+        // since one of the paths to c was /testroot/a2/b2/c
+        a2.save();
+        assertFalse("Saving share-ancestor should save share-descendants",
+                c.isModified());
+    }
+
+    /**
+     * Clone a mix:shareable node to the same workspace multiple times, remove
+     * all parents and save. Exposes an error that occurred when having more
+     * than two members in a shared set and parents were removed in the same
+     * order they were created.
+     */
+    public void testCloneMultipleTimes() throws Exception {
+        final int count = 10;
+        Node[] parents = new Node[count];
+
+        // setup parent nodes and first child
+        for (int i = 0; i < parents.length; i++) {
+            parents[i] = testRootNode.addNode("a" + (i + 1));
+        }
+        Node b = parents[0].addNode("b");
+        testRootNode.save();
+
+        // add mixin
+        b.addMixin("mix:shareable");
+        b.save();
+
+        Workspace workspace = b.getSession().getWorkspace();
+
+        // clone to all other nodes
+        for (int i = 1; i < parents.length; i++) {
+            workspace.clone(workspace.getName(), b.getPath(),
+                    parents[i].getPath() + "/b", false);
+        }
+
+        // remove all parents and save
+        for (int i = 0; i < parents.length; i++) {
+            parents[i].remove();
+        }
+        testRootNode.save();
     }
 
     //---------------------------------------------------------- utility methods