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/03/19 08:53:00 UTC
svn commit: r638734 - in /jackrabbit/trunk/jackrabbit-core/src:
main/java/org/apache/jackrabbit/core/BatchedItemOperations.java
test/java/org/apache/jackrabbit/core/ShareableNodeTest.java
Author: dpfister
Date: Wed Mar 19 00:52:55 2008
New Revision: 638734
URL: http://svn.apache.org/viewvc?rev=638734&view=rev
Log:
JCR-1104 - JSR 283 support
- shareble nodes (work in progress)
Modified:
jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.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=638734&r1=638733&r2=638734&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 Mar 19 00:52:55 2008
@@ -268,7 +268,17 @@
throw new RepositoryException(msg);
}
- // 4. do clone operation (modify and store affected states)
+ // 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.";
+ log.debug(msg);
+ throw new RepositoryException(msg);
+ }
+
+ // 5. do clone operation (modify and store affected states)
if (!srcState.addShare(destParentState.getNodeId())) {
String msg = "Adding a shareable node twice to the same parent is not supported.";
log.debug(msg);
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=638734&r1=638733&r2=638734&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 Mar 19 00:52:55 2008
@@ -20,11 +20,18 @@
import javax.jcr.Node;
import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Workspace;
import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.ObservationManager;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+import org.apache.jackrabbit.core.observation.SynchronousEventListener;
import org.apache.jackrabbit.test.AbstractJCRTest;
/**
@@ -32,16 +39,18 @@
*/
public class ShareableNodeTest extends AbstractJCRTest {
+ //------------------------------------------------------ specification tests
+
/**
- * Add a child to a shareable node and verify that another node in the
- * same shared set has the same child and is modified when the first
- * one is.
+ * Verifies that Node.getIndex returns the correct index in a shareable
+ * node (6.13).
*/
- public void testAddChild() throws Exception {
+ public void testGetIndex() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
Node b1 = a1.addNode("b1");
+ a2.addNode("b");
testRootNode.save();
// add mixin
@@ -51,7 +60,7 @@
// clone
Workspace workspace = b1.getSession().getWorkspace();
workspace.clone(workspace.getName(), b1.getPath(),
- a2.getPath() + "/b2", true);
+ a2.getPath() + "/b", true);
ArrayList list = new ArrayList();
@@ -59,33 +68,53 @@
while (iter.hasNext()) {
list.add(iter.nextNode());
}
+
assertEquals(list.size(), 2);
b1 = (Node) list.get(0);
Node b2 = (Node) list.get(1);
-
- b1.addNode("c");
- assertTrue(b2.isModified());
- assertTrue(b2.hasNode("c"));
- b1.save();
+ assertEquals(b1.getIndex(), 1);
+ assertEquals(b2.getIndex(), 2);
}
/**
- * Adds the mix:shareable mixin to a node.
+ * Verifies that Node.getName returns the correct name in a shareable node
+ * (6.13).
*/
- public void testAddMixin() throws Exception {
- // setup parent node and first child
- Node a = testRootNode.addNode("a");
- Node b = a.addNode("b");
+ public void testGetName() 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();
- b.addMixin("mix:shareable");
- b.save();
+ // add mixin
+ b1.addMixin("mix:shareable");
+ b1.save();
+
+ // clone
+ Workspace workspace = b1.getSession().getWorkspace();
+ workspace.clone(workspace.getName(), b1.getPath(),
+ a2.getPath() + "/b2", true);
+
+ ArrayList list = new ArrayList();
+
+ NodeIterator iter = ((NodeImpl) b1).getSharedSet();
+ while (iter.hasNext()) {
+ list.add(iter.nextNode());
+ }
+
+ assertEquals(list.size(), 2);
+ b1 = (Node) list.get(0);
+ Node b2 = (Node) list.get(1);
+ assertEquals(b1.getName(), "b1");
+ assertEquals(b2.getName(), "b2");
}
/**
- * Clones a mix:shareable node to the same workspace.
+ * Verifies that Node.getPath returns the correct path in a shareable
+ * node (6.13).
*/
- public void testClone() throws Exception {
+ public void testGetPath() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -100,16 +129,29 @@
Workspace workspace = b1.getSession().getWorkspace();
workspace.clone(workspace.getName(), b1.getPath(),
a2.getPath() + "/b2", true);
+
+ ArrayList list = new ArrayList();
+
+ NodeIterator iter = ((NodeImpl) b1).getSharedSet();
+ while (iter.hasNext()) {
+ list.add(iter.nextNode());
+ }
+
+ assertEquals(list.size(), 2);
+ b1 = (Node) list.get(0);
+ Node b2 = (Node) list.get(1);
+ assertEquals(b1.getPath(), "/testroot/a1/b1");
+ assertEquals(b2.getPath(), "/testroot/a2/b2");
}
-
+
/**
- * Clones a mix:shareable node to the same workspace, with the same
- * parent. This is unsupported in Jackrabbit.
+ * Checks new API Node.getSharedSet() (6.13.1)
*/
- public void testCloneToSameParent() throws Exception {
+ public void testIterateSharedSet() throws Exception {
// setup parent nodes and first child
- Node a = testRootNode.addNode("a");
- Node b1 = a.addNode("b1");
+ Node a1 = testRootNode.addNode("a1");
+ Node a2 = testRootNode.addNode("a2");
+ Node b1 = a1.addNode("b1");
testRootNode.save();
// add mixin
@@ -118,25 +160,39 @@
// clone
Workspace workspace = b1.getSession().getWorkspace();
+ workspace.clone(workspace.getName(), b1.getPath(),
+ a2.getPath() + "/b2", true);
- try {
- workspace.clone(workspace.getName(), b1.getPath(),
- a.getPath() + "/b2", true);
- fail("Cloning inside same parent should fail.");
- } catch (UnsupportedRepositoryOperationException e) {
- // expected
+ NodeIterator iter = ((NodeImpl) b1).getSharedSet();
+ int items = 0;
+ while (iter.hasNext()) {
+ iter.nextNode();
+ items++;
}
+ assertEquals(items, 2);
}
/**
- * Verifies that Node.getIndex returns the correct index in a shareable node.
+ * Adds the mix:shareable mixin to a node (6.13.2).
*/
- public void testGetIndex() throws Exception {
+ public void testAddMixin() throws Exception {
+ // setup parent node and first child
+ Node a = testRootNode.addNode("a");
+ Node b = a.addNode("b");
+ testRootNode.save();
+
+ b.addMixin("mix:shareable");
+ b.save();
+ }
+
+ /**
+ * Checks new API Node.removeShare() (6.13.4).
+ */
+ public void testRemoveShare() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
Node b1 = a1.addNode("b1");
- a2.addNode("b");
testRootNode.save();
// add mixin
@@ -146,7 +202,7 @@
// clone
Workspace workspace = b1.getSession().getWorkspace();
workspace.clone(workspace.getName(), b1.getPath(),
- a2.getPath() + "/b", true);
+ a2.getPath() + "/b2", true);
ArrayList list = new ArrayList();
@@ -158,14 +214,17 @@
assertEquals(list.size(), 2);
b1 = (Node) list.get(0);
Node b2 = (Node) list.get(1);
- assertEquals(b1.getIndex(), 1);
- assertEquals(b2.getIndex(), 2);
+ assertTrue(b1.isSame(b2));
+ assertTrue(b2.isSame(b1));
+
+ ((NodeImpl) b1).removeShare();
+ a1.save();
}
-
+
/**
- * Verifies that Node.getName returns the correct name in a shareable node.
+ * Checks new API Node.removeSharedSet() (6.13.4).
*/
- public void testGetName() throws Exception {
+ public void testRemoveSharedSet() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -181,24 +240,46 @@
workspace.clone(workspace.getName(), b1.getPath(),
a2.getPath() + "/b2", true);
- ArrayList list = new ArrayList();
+ ((NodeImpl) b1).removeSharedSet();
+ testRootNode.save();
+ }
+
+ /**
+ * Invokes Node.removeSharedSet(), but saves only one of the parent nodes
+ * of the shared set. This doesn't need to be supported according to the
+ * specification (6.13.4).
+ */
+ public void testRemoveSharedSetSaveOneParentOnly() 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", true);
- NodeIterator iter = ((NodeImpl) b1).getSharedSet();
- while (iter.hasNext()) {
- list.add(iter.nextNode());
- }
+ ((NodeImpl) b1).removeSharedSet();
- assertEquals(list.size(), 2);
- b1 = (Node) list.get(0);
- Node b2 = (Node) list.get(1);
- assertEquals(b1.getName(), "b1");
- assertEquals(b2.getName(), "b2");
+ try {
+ a1.save();
+ fail("Removing a shared set requires saving all parents.");
+ } catch (ConstraintViolationException e) {
+ // expected
+ }
}
-
+
/**
- * Verifies that Node.getPath returns the correct path in a shareable node.
+ * Verifies that shareable nodes in the same shared set have the same
+ * jcr:uuid (6.13.10).
*/
- public void testGetPath() throws Exception {
+ public void testSameUUID() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -224,15 +305,15 @@
assertEquals(list.size(), 2);
b1 = (Node) list.get(0);
Node b2 = (Node) list.get(1);
- assertEquals(b1.getPath(), "/testroot/a1/b1");
- assertEquals(b2.getPath(), "/testroot/a2/b2");
+ assertTrue(b1.getUUID().equals(b2.getUUID()));
}
/**
- * Verifies that Node.isSame returns <code>true</code> for shareable nodes
- * in the same shared set.
+ * Add a child to a shareable node and verify that another node in the
+ * same shared set has the same child and is modified when the first
+ * one is (6.13.11).
*/
- public void testIsSame() throws Exception {
+ public void testAddChild() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -254,21 +335,22 @@
while (iter.hasNext()) {
list.add(iter.nextNode());
}
-
assertEquals(list.size(), 2);
b1 = (Node) list.get(0);
Node b2 = (Node) list.get(1);
- assertTrue(b1.isSame(b2));
- assertTrue(b2.isSame(b1));
+
+ b1.addNode("c");
+ assertTrue(b2.isModified());
+ assertTrue(b2.hasNode("c"));
+ b1.save();
}
/**
- * Checks Node.removeShare().
+ * Verify that a share cycle is detected (6.13.13).
*/
- public void testRemoveShare() throws Exception {
+ public void testShareCycle() 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();
@@ -276,11 +358,125 @@
b1.addMixin("mix:shareable");
b1.save();
+ // clone underneath b1: this must fail
+ Workspace workspace = b1.getSession().getWorkspace();
+
+ try {
+ workspace.clone(workspace.getName(), b1.getPath(),
+ b1.getPath() + "/c", true);
+ fail("Cloning should create a share cycle.");
+ } catch (RepositoryException e) {
+ // expected
+ }
+ }
+
+ /**
+ * Verifies that observation events are sent only once (6.13.15).
+ */
+ public void testObservation() 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", true);
+ class EventCounter implements SynchronousEventListener {
+
+ private int count;
+
+ public void onEvent(EventIterator events) {
+ while (events.hasNext()) {
+ events.nextEvent();
+ count++;
+ }
+ }
+
+ public int getEventCount() {
+ return count;
+ }
+
+ public void resetCount() {
+ count = 0;
+ }
+ };
+
+ EventCounter el = new EventCounter();
+ ObservationManager om = superuser.getWorkspace().getObservationManager();
+
+ om.addEventListener(el, Event.NODE_ADDED, testRootNode.getPath(),
+ true, null, null, false);
+ b1.addNode("c");
+ b1.save();
+ superuser.getWorkspace().getObservationManager().removeEventListener(el);
+ assertEquals(el.getEventCount(), 1);
+
+ el.resetCount();
+ om.addEventListener(el, Event.NODE_REMOVED, testRootNode.getPath(),
+ true, null, null, false);
+ b1.getNode("c").remove();
+ b1.save();
+ superuser.getWorkspace().getObservationManager().removeEventListener(el);
+ assertEquals(el.getEventCount(), 1);
+
+ el.resetCount();
+ om.addEventListener(el, Event.PROPERTY_ADDED, testRootNode.getPath(),
+ true, null, null, false);
+ b1.setProperty("c", "1");
+ b1.save();
+ superuser.getWorkspace().getObservationManager().removeEventListener(el);
+ assertEquals(el.getEventCount(), 1);
+
+ el.resetCount();
+ om.addEventListener(el, Event.PROPERTY_CHANGED, testRootNode.getPath(),
+ true, null, null, false);
+ b1.setProperty("c", "2");
+ b1.save();
+ superuser.getWorkspace().getObservationManager().removeEventListener(el);
+ assertEquals(el.getEventCount(), 1);
+
+ el.resetCount();
+ om.addEventListener(el, Event.PROPERTY_REMOVED, testRootNode.getPath(),
+ true, null, null, false);
+ b1.getProperty("c").remove();
+ b1.save();
+ superuser.getWorkspace().getObservationManager().removeEventListener(el);
+ assertEquals(el.getEventCount(), 1);
+ }
+
+ /**
+ * Verifies that a lock applies to all nodes in a shared set (6.13.16).
+ */
+ public void testLock() throws Exception {
+ // setup parent nodes and first child
+ Node a1 = testRootNode.addNode("a1");
+ a1.addMixin("mix:lockable");
+ Node a2 = testRootNode.addNode("a2");
+ Node b1 = a1.addNode("b1");
+ testRootNode.save();
+
+ // add mixin
+ b1.addMixin("mix:shareable");
+ b1.addMixin("mix:lockable");
+ b1.save();
+
+ // add child c
+ Node c = b1.addNode("c");
+ b1.save();
+
+ // clone
+ Workspace workspace = b1.getSession().getWorkspace();
+ workspace.clone(workspace.getName(), b1.getPath(),
+ a2.getPath() + "/b2", true);
+
ArrayList list = new ArrayList();
NodeIterator iter = ((NodeImpl) b1).getSharedSet();
@@ -291,41 +487,53 @@
assertEquals(list.size(), 2);
b1 = (Node) list.get(0);
Node b2 = (Node) list.get(1);
- assertTrue(b1.isSame(b2));
- assertTrue(b2.isSame(b1));
- ((NodeImpl) b1).removeShare();
- a1.save();
+ // lock shareable node -> all nodes in shared set are locked
+ b1.lock(false, true);
+ assertTrue(b2.isLocked());
+ b1.unlock();
+
+ // deep-lock parent -> locks (common) child node
+ a1.lock(true, true);
+ assertTrue(c.isLocked());
+ a1.unlock();
}
-
+
/**
- * Checks Node.removeSharedSet().
+ * Clones a mix:shareable node to the same workspace (6.13.20). Verifies
+ * that cloning without mix:shareable fails.
*/
- public void testRemoveSharedSet() throws Exception {
+ public void testClone() 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();
+ // clone (1st attempt, without mix:shareable)
+ Workspace workspace = b1.getSession().getWorkspace();
+ try {
+ workspace.clone(workspace.getName(), b1.getPath(),
+ a2.getPath() + "/b2", true);
+ fail("Cloning a node into the same workspace should fail.");
+ } catch (RepositoryException e) {
+ // expected
+ }
+
// add mixin
b1.addMixin("mix:shareable");
b1.save();
- // clone
- Workspace workspace = b1.getSession().getWorkspace();
+ // clone (2nd attempt, with mix:shareable)
workspace.clone(workspace.getName(), b1.getPath(),
a2.getPath() + "/b2", true);
-
- ((NodeImpl) b1).removeSharedSet();
- testRootNode.save();
}
/**
- * Invokes Node.removeSharedSet(), but saves only of the parent nodes of
- * the shared set. This is illegal according to the specification (6.13.4).
+ * Verifies that Node.isSame returns <code>true</code> for shareable nodes
+ * in the same shared set (6.13.21)
*/
- public void testRemoveSharedSetSaveOneParentOnly() throws Exception {
+ public void testIsSame() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -341,20 +549,49 @@
workspace.clone(workspace.getName(), b1.getPath(),
a2.getPath() + "/b2", true);
- ((NodeImpl) b1).removeSharedSet();
+ ArrayList list = new ArrayList();
+
+ NodeIterator iter = ((NodeImpl) b1).getSharedSet();
+ while (iter.hasNext()) {
+ list.add(iter.nextNode());
+ }
+
+ assertEquals(list.size(), 2);
+ b1 = (Node) list.get(0);
+ Node b2 = (Node) list.get(1);
+ assertTrue(b1.isSame(b2));
+ assertTrue(b2.isSame(b1));
+ }
+
+ /**
+ * Removes mix:shareable from a shareable node. This is unsupported in
+ * Jackrabbit (6.13.22).
+ */
+ public void testRemoveMixin() throws Exception {
+ // setup parent node and first child
+ Node a = testRootNode.addNode("a");
+ Node b = a.addNode("b");
+ testRootNode.save();
+
+ // add mixin
+ b.addMixin("mix:shareable");
+ b.save();
+ // remove mixin
try {
- a1.save();
- fail("Removing a shared set requires saving all parents.");
- } catch (ConstraintViolationException e) {
- // expected
+ b.removeMixin("mix:shareable");
+ b.save();
+ fail("Removing mix:shareable should fail.");
+ } catch (UnsupportedRepositoryOperationException e) {
+ // expected
}
}
/**
- * Checks Node.getSharedSet().
+ * Verifies that a descendant of a shareable node appears once in the
+ * result set (6.13.23)
*/
- public void testIterateSharedSet() throws Exception {
+ public void testSearch() throws Exception {
// setup parent nodes and first child
Node a1 = testRootNode.addNode("a1");
Node a2 = testRootNode.addNode("a2");
@@ -364,19 +601,56 @@
// add mixin
b1.addMixin("mix:shareable");
b1.save();
-
+
// clone
Workspace workspace = b1.getSession().getWorkspace();
workspace.clone(workspace.getName(), b1.getPath(),
a2.getPath() + "/b2", true);
- NodeIterator iter = ((NodeImpl) b1).getSharedSet();
- int items = 0;
+ // add new referenceable child
+ Node c = b1.addNode("c");
+ c.addMixin(mixReferenceable);
+ b1.save();
+
+ String sql = "SELECT * FROM nt:unstructured WHERE jcr:uuid = '"+c.getUUID()+"'";
+ QueryResult res = workspace.getQueryManager().createQuery(sql, Query.SQL).execute();
+
+ ArrayList list = new ArrayList();
+
+ NodeIterator iter = res.getNodes();
while (iter.hasNext()) {
- iter.nextNode();
- items++;
+ list.add(iter.nextNode());
+ }
+ assertEquals(list.size(), 1);
+ assertTrue(((NodeImpl) list.get(0)).isSame(c));
+ }
+
+ //--------------------------------------------------------- limitation tests
+
+ /**
+ * Clones a mix:shareable node to the same workspace, with the same
+ * parent. This is unsupported in Jackrabbit.
+ */
+ public void testCloneToSameParent() throws Exception {
+ // setup parent nodes and first child
+ Node a = testRootNode.addNode("a");
+ Node b1 = a.addNode("b1");
+ testRootNode.save();
+
+ // add mixin
+ b1.addMixin("mix:shareable");
+ b1.save();
+
+ // clone
+ Workspace workspace = b1.getSession().getWorkspace();
+
+ try {
+ workspace.clone(workspace.getName(), b1.getPath(),
+ a.getPath() + "/b2", true);
+ fail("Cloning inside same parent should fail.");
+ } catch (UnsupportedRepositoryOperationException e) {
+ // expected
}
- assertEquals(items, 2);
}
/**
@@ -426,30 +700,6 @@
session.move(b.getPath(), a2.getPath() + "/b");
session.save();
fail("Moving a mix:shareable should fail.");
- } catch (UnsupportedRepositoryOperationException e) {
- // expected
- }
- }
-
- /**
- * Removes mix:shareable from a shareable node. This is unsupported in
- * Jackrabbit.
- */
- public void testRemoveMixin() throws Exception {
- // setup parent node and first child
- Node a = testRootNode.addNode("a");
- Node b = a.addNode("b");
- testRootNode.save();
-
- // add mixin
- b.addMixin("mix:shareable");
- b.save();
-
- // remove mixin
- try {
- b.removeMixin("mix:shareable");
- b.save();
- fail("Removing mix:shareable should fail.");
} catch (UnsupportedRepositoryOperationException e) {
// expected
}