You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by an...@apache.org on 2010/10/27 14:36:03 UTC

svn commit: r1027938 - in /jackrabbit/trunk/jackrabbit-core/src: main/java/org/apache/jackrabbit/core/state/ test/java/org/apache/jackrabbit/core/state/ test/resources/org/apache/jackrabbit/core/nodetype/xml/

Author: angela
Date: Wed Oct 27 12:36:03 2010
New Revision: 1027938

URL: http://svn.apache.org/viewvc?rev=1027938&view=rev
Log:
JCR-2780  -  Best effort merge if concurrent modifications include changes to mixin types

Added:
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java   (with props)
    jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd   (with props)
Modified:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java?rev=1027938&r1=1027937&r2=1027938&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java Wed Oct 27 12:36:03 2010
@@ -17,12 +17,22 @@
 package org.apache.jackrabbit.core.state;
 
 import org.apache.jackrabbit.core.id.ItemId;
-import org.apache.jackrabbit.core.id.PropertyId;
 import org.apache.jackrabbit.core.id.NodeId;
+import org.apache.jackrabbit.core.id.PropertyId;
+import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
 import org.apache.jackrabbit.spi.Name;
-
+import org.apache.jackrabbit.spi.QNodeDefinition;
+import org.apache.jackrabbit.spi.QNodeTypeDefinition;
+import org.apache.jackrabbit.spi.QPropertyDefinition;
+import org.apache.jackrabbit.spi.commons.name.NameConstants;
+
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Set;
 
 /**
  * Internal utility class used for merging concurrent changes that occurred
@@ -86,11 +96,7 @@ class NodeStateMerger {
                 }
 
                 // mixin types
-                if (!state.getMixinTypeNames().equals(overlayedState.getMixinTypeNames())) {
-                    // the mixins have been modified but by just looking at the diff we
-                    // can't determine where the change happened since the diffs of either
-                    // removing a mixin from the overlayed or adding a mixin to the
-                    // transient state would look identical...
+                if (!mergeMixinTypes(state, overlayedState, context)) {
                     return false;
                 }
 
@@ -189,6 +195,175 @@ class NodeStateMerger {
         }
     }
 
+    /**
+     * 
+     * @param state
+     * @param overlayedState
+     * @return true if the mixin type names are the same in both node states or
+     * if the mixin modifications do not conflict (and could be merged); false
+     * otherwise.
+     */
+    private static boolean mergeMixinTypes(NodeState state, NodeState overlayedState, MergeContext ctx) {
+        Set<Name> mixins = new HashSet<Name>(state.getMixinTypeNames());
+        Set<Name> overlayedMixins = new HashSet<Name>(overlayedState.getMixinTypeNames());
+        if (mixins.equals(overlayedMixins)) {
+            // no net effect modifications at all -> merge child items defined
+            // by the mixins according to the general rule.
+            return true;
+        }
+
+        PropertyId mixinPropId = new PropertyId(state.getNodeId(), NameConstants.JCR_MIXINTYPES);
+
+        boolean mergeDone;
+        if (ctx.isAdded(mixinPropId)) {
+            // jcr:mixinTypes property was created for 'state'.
+            // changes is safe (without need to merge),
+            // - overlayed state doesn't have any mixins OR
+            // - overlayed state got the same (or a subset) added
+            // and non of the items defined by the new mixin(s) collides with
+            // existing items on the overlayed state
+            if (overlayedMixins.isEmpty() || mixins.containsAll(overlayedMixins)) {
+                mixins.removeAll(overlayedMixins);
+                mergeDone = !conflicts(state, mixins, ctx, true);
+            } else {
+                // different mixins added in overlayedState and state
+                // -> don't merge
+                mergeDone = false;
+            }
+        } else if (ctx.isDeleted(mixinPropId)) {
+            // jcr:mixinTypes property was removed in 'state'.
+            // we can't determine if there was any change to mixin types in the
+            // overlayed state.
+            // -> don't merge.
+            mergeDone = false;
+        } else if (ctx.isModified(mixinPropId)) {
+            /* jcr:mixinTypes property was modified in 'state'.
+               NOTE: if the mixins of the overlayed state was modified as well
+               the property (jcr:mixinTypes) cannot not be persisted (stale).
+
+               since there is not way to determine if the overlayed mixins have
+               been modified just check for conflicts related to a net mixin
+               addition.
+             */
+            if (mixins.containsAll(overlayedMixins)) {
+                // net result of modifications is only addition.
+                // -> so far the changes are save if there are no conflicts
+                //    caused by mixins modification in 'state'.
+                //    NOTE: the save may still fail if the mixin property has
+                //    been modified in the overlayed state as well.
+                mixins.removeAll(overlayedMixins);
+                mergeDone = !conflicts(state, mixins, ctx, true);
+            } else {
+                // net result is either a removal in 'state' or modifications
+                // in both node states.
+                // -> don't merge.
+                mergeDone = false;
+            }
+        } else {
+            // jcr:mixinTypes property was added or modified in the overlayed
+            // state but neither added nor modified in 'state'.
+            if (overlayedMixins.containsAll(mixins)) {
+                // the modification in the overlayed state only includes the
+                // addition of mixin node types, but no removal.
+                // -> need to check if any added items from state would
+                //    collide with the items defined by the new mixin on the
+                //    overlayed state.
+                overlayedMixins.removeAll(mixins);
+                if (!conflicts(state, overlayedMixins, ctx, false)) {
+                    // update the mixin names in 'state'. the child items defined
+                    // by the new mixins will be added later on during merge of
+                    // child nodes and properties.
+                    state.setMixinTypeNames(overlayedMixins);
+                    mergeDone = true;
+                } else {
+                    mergeDone = false;
+                }
+            } else {
+                // either remove-mixin(s) or both add and removal of mixin in
+                // the overlayed state.
+                // -> we cannot merge easily
+                mergeDone = false;
+            }
+        }
+
+        return mergeDone;
+    }
+
+    /**
+     *
+     * @param state The state of the node to be saved.
+     * @param addedMixins The added mixins to be used for testing
+     * @param ctx
+     * @param compareToOverlayed
+     * @return true if a conflict can be determined, false otherwise.
+     */
+    private static boolean conflicts(NodeState state,
+                                     Set<Name> addedMixins,
+                                     MergeContext ctx, boolean compareToOverlayed) {
+        try {
+            // check for all added mixin types in one state if there are colliding
+            // child items in the other state.
+            // this is currently a simple check for named item definitions;
+            // if the mixin defines residual item definitions -> return false.
+            for (Name mixinName : addedMixins) {
+                EffectiveNodeType ent = ctx.getEffectiveNodeType(mixinName);
+
+                if (ent.getUnnamedItemDefs().length > 0) {
+                    // the mixin defines residual child definitions -> cannot
+                    // easily determine conflicts
+                    return false;
+                }
+
+                NodeState overlayed = (NodeState) state.getOverlayedState();
+                for (ChildNodeEntry cne : state.getChildNodeEntries()) {
+                    if (ent.getNamedNodeDefs(cne.getName()).length > 0) {
+                        if (ctx.isAdded(cne.getId()) || isAutoCreated(cne, ent)) {
+                            if (!compareToOverlayed || overlayed.hasChildNodeEntry(cne.getName())) {
+                                return true;
+                            }
+                        } // else: neither added nor autocreated in 'state' .
+
+                    } // else: child node not defined by the added mixin type
+                }
+
+                for (Name propName : state.getPropertyNames()) {
+                    if (ent.getNamedPropDefs(propName).length > 0) {
+                        PropertyId pid = new PropertyId(state.getNodeId(), propName);
+                        if (ctx.isAdded(pid) || isAutoCreated(propName, ent)) {
+                            if (!compareToOverlayed || overlayed.hasPropertyName(propName)) {
+                                return true;
+                            }
+                        } // else: neither added nor autocreated in 'state'
+                    } // else: property not defined by added mixin
+                }
+            }
+        } catch (NoSuchNodeTypeException e) {
+            // unable to determine collision
+            return true;
+        }
+
+        // no conflict detected
+        return false;
+    }
+
+    private static boolean isAutoCreated(ChildNodeEntry cne, EffectiveNodeType ent) {
+        for (QNodeDefinition def : ent.getAutoCreateNodeDefs()) {
+            if (def.getName().equals(cne.getName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isAutoCreated(Name propertyName, EffectiveNodeType ent) {
+        for (QPropertyDefinition def : ent.getAutoCreatePropDefs()) {
+            if (def.getName().equals(propertyName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     //-----------------------------------------------------< inner interfaces >
 
     /**
@@ -199,5 +374,6 @@ class NodeStateMerger {
         boolean isDeleted(ItemId id);
         boolean isModified(ItemId id);
         boolean allowsSameNameSiblings(NodeId id);
+        EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException;
     }
 }

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java?rev=1027938&r1=1027937&r2=1027938&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java Wed Oct 27 12:36:03 2010
@@ -17,6 +17,7 @@
 package org.apache.jackrabbit.core.state;
 
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
@@ -24,6 +25,7 @@ import javax.jcr.PropertyType;
 import javax.jcr.ReferentialIntegrityException;
 import javax.jcr.RepositoryException;
 import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.NodeType;
 
 import org.apache.jackrabbit.core.RepositoryImpl;
 import org.apache.jackrabbit.core.cluster.UpdateEventChannel;
@@ -246,9 +248,9 @@ public class SharedItemStateManager
     public ItemState getItemState(ItemId id)
             throws NoSuchItemStateException, ItemStateException {
         // check the virtual root ids (needed for overlay)
-        for (int i = 0; i < virtualProviders.length; i++) {
-            if (virtualProviders[i].isVirtualRoot(id)) {
-                return virtualProviders[i].getItemState(id);
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            if (virtualProvider.isVirtualRoot(id)) {
+                return virtualProvider.getItemState(id);
             }
         }
 
@@ -266,9 +268,9 @@ public class SharedItemStateManager
         }
 
         // check if there is a virtual state for the specified item
-        for (int i = 0; i < virtualProviders.length; i++) {
-            if (virtualProviders[i].hasItemState(id)) {
-                return virtualProviders[i].getItemState(id);
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            if (virtualProvider.hasItemState(id)) {
+                return virtualProvider.getItemState(id);
             }
         }
 
@@ -280,8 +282,8 @@ public class SharedItemStateManager
      */
     public boolean hasItemState(ItemId id) {
         // check the virtual root ids (needed for overlay)
-        for (int i = 0; i < virtualProviders.length; i++) {
-            if (virtualProviders[i].isVirtualRoot(id)) {
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            if (virtualProvider.isVirtualRoot(id)) {
                 return true;
             }
         }
@@ -307,8 +309,8 @@ public class SharedItemStateManager
         }
 
         // otherwise check virtual ones
-        for (int i = 0; i < virtualProviders.length; i++) {
-            if (virtualProviders[i].hasItemState(id)) {
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            if (virtualProvider.hasItemState(id)) {
                 return true;
             }
         }
@@ -334,9 +336,9 @@ public class SharedItemStateManager
         }
 
         // check virtual providers
-        for (int i = 0; i < virtualProviders.length; i++) {
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
             try {
-                return virtualProviders[i].getNodeReferences(id);
+                return virtualProvider.getNodeReferences(id);
             } catch (NoSuchItemStateException e) {
                 // ignore
             }
@@ -370,8 +372,8 @@ public class SharedItemStateManager
         }
 
         // check virtual providers
-        for (int i = 0; i < virtualProviders.length; i++) {
-            if (virtualProviders[i].hasNodeReferences(id)) {
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            if (virtualProvider.hasNodeReferences(id)) {
                 return true;
             }
         }
@@ -449,8 +451,8 @@ public class SharedItemStateManager
      */
     public void dispose() {
         // remove virtual item state providers (see JCR-2023)
-        for (int i = 0; i < virtualProviders.length; i++) {
-            virtualProviders[i].removeListener(this);
+        for (VirtualItemStateProvider virtualProvider : virtualProviders) {
+            virtualProvider.removeListener(this);
         }
         virtualProviders = new VirtualItemStateProvider[0];
 
@@ -638,6 +640,10 @@ public class SharedItemStateManager
                                             }
                                         }
 
+                                        public EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException {
+                                            return ntReg.getEffectiveNodeType(ntName);
+                                        }
+
                                         protected NodeState getNodeState(NodeId id)
                                                 throws ItemStateException {
                                             if (local.has(id)) {
@@ -1422,7 +1428,7 @@ public class SharedItemStateManager
     private boolean isShareable(NodeState state) throws RepositoryException {
         // shortcut: check some wellknown built-in types first
         Name primary = state.getNodeTypeName();
-        Set mixins = state.getMixinTypeNames();
+        Set<Name> mixins = state.getMixinTypeNames();
         if (mixins.contains(NameConstants.MIX_SHAREABLE)) {
             return true;
         }

Added: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java?rev=1027938&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java Wed Oct 27 12:36:03 2010
@@ -0,0 +1,978 @@
+/*
+ * 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.core.state;
+
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.test.AbstractJCRTest;
+
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeIterator;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * <code>NodeStateMergerTest</code>...
+ */
+public class NodeStateMergerTest extends AbstractJCRTest {
+
+    /** Name of the cnd nodetype file for import and namespace registration. */
+    private static final String TEST_NODETYPES = "org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd";
+
+    private List<String> testMixins = new ArrayList<String>();
+
+    private Node testNode;
+
+    private Session sessionB;
+    private Node testNodeB;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Reader cnd = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES));
+        CndImporter.registerNodeTypes(cnd, superuser);
+        cnd.close();
+
+        NodeTypeIterator it = superuser.getWorkspace().getNodeTypeManager().getMixinNodeTypes();
+        while (it.hasNext()) {
+            NodeType nt = it.nextNodeType();
+            if (nt.getName().startsWith("test:")) {
+                testMixins.add(nt.getName());
+            }
+        }
+
+        testNode = testRootNode.addNode(nodeName1, "nt:unstructured");
+        superuser.save();
+
+        sessionB = getHelper().getSuperuserSession();
+        testNodeB = sessionB.getNode(testNode.getPath());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (sessionB != null) {
+            sessionB.logout();
+        }
+        super.tearDown();
+    }
+
+    /**
+     * Add the same property with both sessions but with different values.
+     *
+     * @throws RepositoryException
+     */
+    public void testAddSamePropertiesWithDifferentValues() throws RepositoryException {
+        assertFalse(testNode.hasProperty(propertyName2));
+        assertFalse(testNodeB.hasProperty(propertyName2));
+
+        testNode.setProperty(propertyName2, "value");
+
+        testNodeB.setProperty(propertyName2, "otherValue");
+        sessionB.save();
+
+        superuser.save();
+
+        assertEquals("value", testNode.getProperty(propertyName2).getString());
+        testNodeB.refresh(false);
+        assertEquals("value", testNodeB.getProperty(propertyName2).getString());
+    }
+
+    /**
+     * Modify the same property with both sessions but with different values.
+     *
+     * @throws RepositoryException
+     */
+    public void testModifySamePropertiesWithDifferentValues() throws RepositoryException {
+        testNode.setProperty(propertyName2, "test");
+        superuser.save();
+        testNodeB.refresh(false);
+
+        assertTrue(testNodeB.hasProperty(propertyName2));
+
+        try {
+            testNode.setProperty(propertyName2, "value");
+
+            testNodeB.setProperty(propertyName2, "otherValue");
+            sessionB.save();
+
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    //------------------ Tests adding jcr:mixinType property in the session1 ---
+    /**
+     * Both node don't have any mixins assigned yet. Test if adding the same
+     * mixin node with both sessions works.
+     *
+     * @throws RepositoryException
+     */
+    public void testAddSameMixinToSessionsAB() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addMixin(mixReferenceable);
+
+        testNodeB.addMixin(mixReferenceable);
+        sessionB.save();
+
+        superuser.save();
+    }
+
+    /**
+     * Same as {@link #testAddSameMixinToSessionsAB} but in addition
+     * adding non-conflicting properties defined by this mixin. The properties
+     * must be merged silently.
+     *
+     * @throws RepositoryException
+     */
+    public void testAddSameMixinToSessionsAB2() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addMixin("test:mixinProp_1");
+        testNode.setProperty("test:prop_double", 124);
+
+        testNodeB.addMixin("test:mixinProp_1");
+        testNodeB.setProperty("test:prop_string", "abc");
+        sessionB.save();
+
+        superuser.save();
+
+        assertEquals("abc", testNode.getProperty("test:prop_string").getString());
+        testNodeB.refresh(false);
+        assertEquals(124, testNodeB.getProperty("test:prop_double").getLong());
+    }
+
+    /**
+     * Same as {@link #testAddSameMixinToSessionsAB} but in addition
+     * a property defined by this mixin with different value. Value of overlayed
+     * property (from sessionB) must be merged into sessionA.
+     *
+     * @throws RepositoryException
+     */
+    public void testAddSameMixinToSessionsAB3() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addMixin("test:mixinProp_1");
+        testNode.setProperty("test:prop_double", 124);
+
+        testNodeB.addMixin("test:mixinProp_1");
+        testNodeB.setProperty("test:prop_double", 134);
+        sessionB.save();
+
+        superuser.save();
+
+        assertEquals(124, testNode.getProperty("test:prop_double").getLong());
+        testNodeB.refresh(false);
+        assertEquals(124, testNodeB.getProperty("test:prop_double").getLong());
+    }
+
+    /**
+     * Same as {@link #testAddSameMixinToSessionsAB3} having additional
+     * modifications in the overlayed state (modifications by session B).
+     *
+     * @throws RepositoryException
+     */
+    public void testAddSameMixinToSessionsAB4() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addMixin("test:mixinProp_1");
+        testNode.setProperty("test:prop_double", 124);
+
+        testNodeB.addMixin("test:mixinProp_1");
+        testNodeB.setProperty("test:prop_double", 134);
+        testNodeB.setProperty("more", "yes");
+        sessionB.save();
+
+        superuser.save();
+
+        assertEquals(124, testNode.getProperty("test:prop_double").getLong());
+        testNodeB.refresh(false);
+        assertEquals(124, testNodeB.getProperty("test:prop_double").getLong());
+    }
+
+    /**
+     * Test adding mixin(s) to sessionA while the overlayed
+     * (attached to testNode2, sessionB) doesn't have any mixins defined.
+     * The changes should be merged as there is no conflict.
+     *
+     * @throws RepositoryException
+     */
+    public void testMixinAddedInSessionA() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        for (String mixin : testMixins) {
+            testNode.addMixin(mixin);
+        }
+
+        testNodeB.addNode(nodeName1, "nt:unstructured");
+        testNodeB.setProperty(propertyName1, "anyValue");
+        sessionB.save();
+
+        superuser.save();
+
+        assertTrue(testNode.hasNode(nodeName1));
+        assertTrue(testNode.hasProperty(propertyName1));
+
+        testNodeB.refresh(false);
+        for (String mixin : testMixins) {
+            assertTrue(testNodeB.isNodeType(mixin));
+        }
+    }
+
+    /**
+     * Test adding mixin(s) to sessionA, while the other sessionB
+     * doesn't have any mixins defined. The changes should be merged as there
+     * is no conflict.
+     * 
+     * @throws RepositoryException
+     */
+    public void testMixinAddedInSessionA2() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.setProperty("test:prop_double", sessionB.getValueFactory().createValue(false));
+        sessionB.save();
+
+        superuser.save();
+        assertTrue(testNode.isNodeType("test:mixinProp_1"));
+        assertEquals(PropertyType.BOOLEAN, testNode.getProperty("test:prop_double").getType());
+
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.isNodeType("test:mixinProp_1"));
+        assertEquals(PropertyType.BOOLEAN, testNodeB.getProperty("test:prop_double").getType());
+        assertEquals(testNode.getProperty("test:prop_double").getString(), testNodeB.getProperty("test:prop_double").getString());
+        assertEquals("nt:unstructured", testNode.getProperty("test:prop_double").getDefinition().getDeclaringNodeType().getName());
+    }
+
+    /**
+     * Test adding mixin(s) to sessionA while sessionB doesn't add new mixins
+     * but has new child items that collide with the items defined by the new mixin.
+     * The merge should in this case fail.
+     *
+     * @throws RepositoryException
+     */
+    public void testAddedInSessionAConflictsWithChildItemsInSessionB() throws RepositoryException {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+        
+        testNode.addMixin("test:mixinProp_5"); // has an autocreated property
+
+        testNodeB.setProperty("test:prop_long_p", "conflict");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+            assertFalse(testNodeB.isNodeType("test:mixinProp_5"));
+            testNodeB.refresh(false);
+            assertEquals("conflict", testNodeB.getProperty("test:prop_long_p").getString());
+        }
+    }
+
+    /**
+     * Similar to {@link #testAddedInSessionAConflictsWithChildItemsInSessionB}
+     * but adding mix:referenceable in the SessionA while SessionB
+     * gets a property jcr:uuid (with different type).
+     * 
+     * @throws RepositoryException
+     */
+// TODO: uncomment once JCR-2779 is fixed
+//    public void testAddedReferenceableSessionAConflictsWithPropInSessionB() throws RepositoryException {
+//        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+//
+//        testNode.addMixin(mixReferenceable);
+//
+//        Property p = testNode2.setProperty("jcr:uuid", session2.getValueFactory().createValue(false));
+//        assertTrue(testNode2.hasProperty("jcr:uuid"));
+//        assertTrue(p.isNew());
+//        session2.save();
+//
+//        assertTrue(testNode2.hasProperty("jcr:uuid"));
+//        assertTrue(p.isNew());
+//        try {
+//            superuser.save();
+//            fail();
+//        } catch (InvalidItemStateException e) {
+//            assertTrue(testNode.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.STRING, testNode.getProperty("jcr:uuid").getType());
+//        }
+//    }
+
+    /**
+     * Same as {@link #testAddedInSessionAConflictsWithChildItemsInSessionB}
+     * but in addition the overlayed state gets an addition child node.
+     *
+     * @throws RepositoryException
+     */
+// TODO: uncomment once JCR-2779 is fixed
+//    public void testAddedReferenceableSessionAConflictsWithPropInSessionB2() throws RepositoryException {
+//        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+//
+//        testNode.addMixin(mixReferenceable);
+//
+//        testNode2.setProperty("jcr:uuid", session2.getValueFactory().createValue(false));
+//        testNode2.addNode("test");
+//        session2.save();
+//
+//        assertTrue(testNode2.hasProperty("jcr:uuid"));
+//        assertTrue(testNode2.getProperty("jcr:uuid").isNew());
+//        try {
+//            superuser.save();
+//            fail();
+//        } catch (InvalidItemStateException e) {
+//            assertTrue(testNode.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.STRING, testNode.getProperty("jcr:uuid").getType());
+//        }
+//    }
+
+    /**
+     * Adding different node types in the SessionA and SessionB:
+     * Not handled and thus the merge must fail.
+     * 
+     * @throws RepositoryException
+     */
+    public void testDifferentMixinAddedInSessionAB() throws Exception {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+        
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.addMixin("test:mixinProp_3");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail("Different mixin types added both in SessionA and SessionB. InvalidItemStateException expected.");
+        } catch (InvalidItemStateException e) {
+            // expected
+            assertFalse(testNode.isNodeType("test:mixinProp_3"));
+        }
+    }
+
+    //---------------- Tests removing jcr:mixinType property in the SessionA ---
+    /**
+     * Remove the jcr:mixinType property by removing all mixin types. Merge
+     * to changes made in the overlayed state should fail.
+     * @throws Exception
+     */
+    public void testMixinRemovedInSessionA() throws Exception {
+        for (int i = 1; i<=5; i++) {
+            testNode.addMixin("test:mixinProp_" + i);
+        }    
+        superuser.save();
+        testNodeB.refresh(false);
+
+        // remove all mixin types
+        for (NodeType mixin : testNode.getMixinNodeTypes()) {
+            testNode.removeMixin(mixin.getName());
+        }
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNodeB.addNode(nodeName1, "nt:unstructured");
+        testNodeB.setProperty(propertyName1, "anyValue");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinRemovedInSessionA} with different mixin types.
+     * @throws Exception
+     */
+    public void testMixinRemovedInSessionA2() throws Exception {
+        for (int i = 1; i<=4; i++) {
+            testNode.addMixin("test:mixinNode_" + i);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+
+        // remove all mixin types
+        for (NodeType mixin : testNode.getMixinNodeTypes()) {
+            testNode.removeMixin(mixin.getName());
+        }
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNodeB.addNode(nodeName1, "nt:unstructured");
+        testNodeB.setProperty(propertyName1, "anyValue");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    //--------------- Tests modifying jcr:mixinType property in the SessionA ---
+    /**
+     * Modify the existing mixin property in the SessionA change without adding
+     * conflicting modifications to SessionB.
+     * 
+     * @throws Exception
+     */
+    public void testMixinModifiedInSessionA() throws Exception {
+        testNode.addMixin("test:mixinProp_5");    
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modify the mixin types
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.addNode(nodeName1, "nt:unstructured");
+        testNodeB.setProperty(propertyName1, "anyValue");
+        sessionB.save();
+
+        superuser.save();
+
+        assertTrue(testNode.hasProperty(propertyName1));
+        assertTrue(testNode.hasNode(nodeName1));
+
+        assertTrue(testNodeB.isNodeType("test:mixinProp_1"));
+        assertTrue(Arrays.asList(testNodeB.getMixinNodeTypes()).contains(sessionB.getWorkspace().getNodeTypeManager().getNodeType("test:mixinProp_1")));
+    }
+
+    public void testMixinModifiedInSessionAB() throws Exception {
+        testNode.addMixin("test:mixinProp_5");
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modify the mixin types
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.addMixin("test:mixinProp_1");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    public void testMixinModifiedInSessionAB2() throws Exception {
+        testNode.addMixin("test:mixinProp_5");
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modify the mixin types
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.removeMixin("test:mixinProp_5");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+            assertTrue(testNode.hasProperty("jcr:mixinTypes"));
+            assertTrue(testNode.isNodeType("test:mixinProp_1"));
+            assertTrue(testNode.isNodeType("test:mixinProp_5"));
+            assertEquals(2, testNode.getMixinNodeTypes().length);
+        }
+    }
+
+    public void testMixinModifiedInSessionAB3() throws Exception {
+        testNode.addMixin("test:mixinProp_5");
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modify the mixin types
+        testNode.addMixin("test:mixinProp_1");
+        testNode.setProperty(propertyName1, "value");
+
+        testNodeB.removeMixin("test:mixinProp_5");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+            assertTrue(testNode.hasProperty("jcr:mixinTypes"));
+            assertTrue(testNode.isNodeType("test:mixinProp_1"));
+            assertTrue(testNode.isNodeType("test:mixinProp_5"));
+            assertEquals(2, testNode.getMixinNodeTypes().length);
+        }
+    }
+
+    public void testMixinModifiedInSessionAB4() throws Exception {
+        testNode.addMixin("test:mixinProp_5");
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modify the mixin types
+        testNode.addMixin("test:mixinProp_1");
+
+        testNodeB.addMixin("test:mixinProp_2");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        } 
+    }
+
+    //-------------------------- Tests adding jcr:mixinType only in SessionB ---
+    /**
+     * No jcr:mixinTypes property present in the SessionA but was added
+     * to the overlayed state while other changes were made to the SessionA.
+     * @throws Exception
+     */
+    public void testMixinAddedInSessionB() throws Exception {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addNode(nodeName2);
+
+        testNodeB.addMixin("test:mixinProp_1");
+        sessionB.save();
+
+        superuser.save();
+
+        assertTrue(testNode.isNodeType("test:mixinProp_1"));
+        assertTrue(testNode.hasProperty("jcr:mixinTypes"));
+
+        assertTrue(testNodeB.hasNode(nodeName2));
+    }
+
+    /**
+     * Same as {@link #testMixinAddedInSessionB} but having 2 SessionA modifications.
+     * @throws Exception
+     */
+    public void testMixinAddedInSessionB2() throws Exception {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+
+        testNode.addNode(nodeName2);
+        testNode.setProperty(propertyName1, "value");
+
+        testNodeB.addMixin("test:mixinProp_1");
+        sessionB.save();
+
+        superuser.save();
+
+        assertTrue(testNode.isNodeType("test:mixinProp_1"));
+        assertTrue(testNode.hasProperty("jcr:mixinTypes"));
+
+        assertTrue(testNodeB.hasNode(nodeName2));
+    }
+
+    /**
+     * Add the mixin property in SessionB by adding a single mixin
+     * and create a conflicting item in the SessionA -> merge must fail.
+     * @throws Exception
+     */
+    public void testMixinAddedInSessionBWithConflictingChanges() throws Exception {
+        assertFalse(testNode.hasProperty("jcr:mixinTypes"));
+        
+        testNode.addNode(nodeName2);
+        testNode.setProperty("test:prop_long_p", "value");
+
+        testNodeB.addMixin("test:mixinProp_5");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    //----------------------- Test jcr:mixinTypes removed in SessionB ----------
+    /**
+     * Test if removing the (existing) jcr:mixinTypes property in the overlayed
+     * state (by removing all mixins) is not merged to SessionA changes.
+     * @throws Exception
+     */
+    public void testMixinRemovedInSessionB() throws Exception {
+        for (int i = 1; i<=5; i++) {
+            testNode.addMixin("test:mixinProp_" + i);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+
+        testNode.addNode(nodeName1, "nt:unstructured");
+        testNode.setProperty(propertyName1, "anyValue");
+
+        // remove all mixin types
+        for (NodeType mixin : testNodeB.getMixinNodeTypes()) {
+            testNodeB.removeMixin(mixin.getName());
+        }
+        assertFalse(testNodeB.hasProperty("jcr:mixinTypes"));
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    //----------------------- Test mixin modification (add-only) in SessionB ---
+    /**
+     * Existing mixins are modified in SessionB but not in the
+     * SessionA, where the net effect is 'add-only' in the SessionA. 
+     * Changes should be merge if there are no conflicting SessionA
+     * modifications.
+     * 
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionB() throws Exception {
+        for (int i = 1; i<=5; i++) {
+            testNode.addMixin("test:mixinProp_" + i);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // non-conflicting modification in SessionA
+        testNode.setProperty(propertyName1, "value");
+
+        testNodeB.addMixin("test:mixinNode_1");
+        sessionB.save();
+
+        superuser.save();
+
+        assertTrue(testNode.isNodeType("test:mixinNode_1"));
+        List<NodeType> mx = Arrays.asList(testNode.getMixinNodeTypes());
+        assertTrue(mx.contains(superuser.getWorkspace().getNodeTypeManager().getNodeType("test:mixinNode_1")));
+
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty(propertyName1));
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionB} with different
+     * SessionA modifications.
+     *
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionB2() throws Exception {
+        for (int i = 1; i<=5; i++) {
+            testNode.addMixin("test:mixinProp_" + i);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // non-conflicting modification in SessionA
+        testNode.setProperty(propertyName1, "value");
+        testNode.addNode(nodeName2);
+
+        testNodeB.addMixin("test:mixinNode_1");
+        sessionB.save();
+
+        superuser.save();
+        assertTrue(testNode.isNodeType("test:mixinNode_1"));
+        List<NodeType> mx = Arrays.asList(testNode.getMixinNodeTypes());
+        assertTrue(mx.contains(superuser.getWorkspace().getNodeTypeManager().getNodeType("test:mixinNode_1")));
+
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty(propertyName1));
+        assertTrue(testNodeB.hasNode(nodeName2));
+    }
+
+    /**
+     * Test if the merge of add-only mixin modification in the overlayed stated
+     * is aborted if there are conflicting SessionA changes present.
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionBWithConflictingChanges() throws Exception {
+        testNode.addMixin(mixReferenceable);
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+   
+        // conflicting modification in SessionA
+        testNode.setProperty(propertyName1, "value");
+        testNode.addNode("test:child_1");
+
+        testNodeB.addMixin("test:mixinNode_1");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges}
+     * with different SessionA modifications.
+     *
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionBWithConflictingChanges2() throws Exception {
+        testNode.addMixin(mixReferenceable);
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // conflicting modification in SessionA
+        testNode.addNode("test:child_1");
+
+        testNodeB.addMixin("test:mixinNode_1");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges}
+     * with different mixin and SessionA modifications.
+     *
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionBWithConflictingChanges3() throws Exception {
+        testNode.addMixin(mixReferenceable);
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // conflicting modification in SessionA
+        testNode.setProperty("test:prop_long_p", "non-long-value");
+
+        testNodeB.addMixin("test:mixinProp_5");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges}
+     * with different mixin and other kind of modifications.
+     *
+     * @throws Exception
+     */
+    public void testMixinModifiedAddInSessionBWithConflictingChanges4() throws Exception {
+        testNode.addMixin(mixReferenceable);
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // conflicting modification in session1
+        testNode.setProperty("test:prop_long_p", "non-long-value");        
+        testNode.addNode("test:child_1");
+
+        testNodeB.addMixin("test:mixinProp_5");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges}
+     * but using mix:referencable as mixin to force the modification in the
+     * overlayed state.
+     *
+     * @throws Exception
+     */
+// TODO: uncomment once JCR-2779 is fixed
+//    public void testMixinModifiedReferenceableInSessionBConflicting() throws RepositoryException {
+//        testNode.addMixin("test:mixinProp_1");
+//        superuser.save();
+//        assertTrue(testNode2.hasProperty("jcr:mixinTypes"));
+//
+//        testNode.setProperty("jcr:uuid", superuser.getValueFactory().createValue(false));
+//
+//        testNode2.addMixin(mixReferenceable);
+//        session2.save();
+//
+//        assertTrue(testNode.hasProperty("jcr:uuid"));
+//        assertTrue(testNode.getProperty("jcr:uuid").isNew());
+//        try {
+//            superuser.save();
+//            fail();
+//        } catch (InvalidItemStateException e) {
+//            assertFalse(testNode.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.BOOLEAN, testNode.getProperty("jcr:uuid").getType());
+//
+//            assertTrue(testNode2.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.STRING, testNode2.getProperty("jcr:uuid").getType());
+//        }
+//    }
+
+    /**
+     * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges}
+     * but using mix:referencable as mixin to force the modification in the
+     * overlayed state.
+     *
+     * @throws Exception
+     */
+// TODO: uncomment once JCR-2779 is fixed
+//    public void testMixinModifiedReferenceableInSessionBConflicting2() throws RepositoryException {
+//        testNode.addMixin("test:mixinProp_1");
+//        superuser.save();
+//        assertTrue(testNode2.hasProperty("jcr:mixinTypes"));
+//
+//        testNode.setProperty("jcr:uuid", superuser.getValueFactory().createValue(false));
+//        testNode.addNode(nodeName2);
+//
+//        testNode2.addMixin(mixReferenceable);
+//        session2.save();
+//
+//        assertTrue(testNode.hasProperty("jcr:uuid"));
+//        assertTrue(testNode.getProperty("jcr:uuid").isNew());
+//        try {
+//            superuser.save();
+//            fail();
+//        } catch (InvalidItemStateException e) {
+//            assertFalse(testNode.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.BOOLEAN, testNode.getProperty("jcr:uuid").getType());
+//
+//            assertTrue(testNode2.isNodeType(mixReferenceable));
+//            assertEquals(PropertyType.STRING, testNode2.getProperty("jcr:uuid").getType());
+//        }
+//    }
+
+    //-------------------- Test mixin modification (remove-only) in SessionB ---
+    /**
+     * Test if merge fails if some mixin removal occurred in the SessionB.
+     *
+     * @throws Exception
+     */
+    public void testMixinModifiedRemovedInSessionB() throws Exception {
+        for (String mixin : testMixins) {
+            testNode.addMixin(mixin);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+
+        // modification in session1
+        testNode.setProperty(propertyName1, "value");
+
+        // mixin-removal in the session2
+        testNodeB.removeMixin("test:mixinProp_1");
+        testNodeB.removeMixin("test:mixinProp_2");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Same as {@link #testMixinModifiedRemovedInSessionB} but with different
+     * SessionA modifications.
+     * 
+     * @throws Exception
+     */
+    public void testMixinModifiedRemovedInSessionB2() throws Exception {
+        for (String mixin : testMixins) {
+            testNode.addMixin(mixin);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+
+        // modification in SessionA
+        testNode.setProperty(propertyName1, "value");
+        testNode.addNode(nodeName2);
+
+        // mixin-removal in SessionB
+        testNodeB.removeMixin("test:mixinProp_1");
+        testNodeB.removeMixin("test:mixinProp_2");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+
+    //---------------------------- Test other mixin modification in SessionB ---
+    /**
+     * Test if merge fails if the mixins of the overlayed state (sessionB) were
+     * modified in a combination of add and removal of mixin.
+     * 
+     * @throws Exception
+     */
+    public void testMixinModifiedInSessionB() throws Exception {
+        for (String mixin : testMixins) {
+            testNode.addMixin(mixin);
+        }
+        superuser.save();
+        testNodeB.refresh(false);
+        assertTrue(testNodeB.hasProperty("jcr:mixinTypes"));
+
+        // modification in SessionA
+        testNode.setProperty(propertyName1, "value");
+
+        // mixin-removal in the SessionB
+        testNodeB.addMixin(mixReferenceable);
+        testNodeB.removeMixin("test:mixinProp_2");
+        sessionB.save();
+
+        try {
+            superuser.save();
+            fail();
+        } catch (InvalidItemStateException e) {
+            // expected
+        }
+    }
+}
\ No newline at end of file

Propchange: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision Rev URL

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java?rev=1027938&r1=1027937&r2=1027938&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java Wed Oct 27 12:36:03 2010
@@ -40,6 +40,7 @@ public class TestAll extends TestCase {
         suite.addTestSuite(DefaultISMLockingDeadlockTest.class);
         suite.addTestSuite(FineGrainedISMLockingTest.class);
         suite.addTestSuite(NameSetTest.class);
+        suite.addTestSuite(NodeStateMergerTest.class);
 
         return suite;
     }

Added: jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd?rev=1027938&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd (added)
+++ jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd Wed Oct 27 12:36:03 2010
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+<test = "http://www.apache.org/jackrabbit/test">
+
+[test:mixinProp_1] mixin
+  - test:prop_double (double)
+  - test:prop_string (string)
+
+[test:mixinProp_2] mixin
+  - test:prop_name_p (name) protected
+
+[test:mixinProp_3] mixin
+  - test:prop_boolean_p (boolean) = 'true' protected autocreated
+
+[test:mixinProp_4] mixin
+  - * (string)
+
+[test:mixinProp_5] mixin
+  - test:prop_long_p (long) = '123' autocreated
+
+
+
+[test:mixinNode_1] mixin
+  + test:child_1 (nt:unstructured) = nt:unstructured
+
+[test:mixinNode_2] mixin
+  + test:child_2 (nt:unstructured) = nt:unstructured protected
+
+[test:mixinNode_3] mixin
+  + test:child_3 (nt:unstructured) = nt:unstructured protected autocreated
+
+[test:mixinNode_4] mixin
+  + * (nt:unstructured) = nt:unstructured
\ No newline at end of file

Propchange: jackrabbit/trunk/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd
------------------------------------------------------------------------------
    svn:eol-style = native