Index: b/docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml =================================================================== --- a/docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml (revision ) +++ b/docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml (working copy) @@ -328,6 +328,133 @@ configuration.loadFrom(configSource); + + Repository system content + + Each JCR repository contains information about the system in the "/jcr:system" area of the repository content. + All of this system content applies to the whole repository (e.g., namespaces, node types, locks, versions, etc.) and + therefore every session for each workspace sees the exact same "/jcr:system" content. + + + ModeShape implements this behavior by storing all "/jcr:system" content in a separate workspace, and then + using federation to project that content into each workspace. This ensures + that all workspaces see the same content, without having to duplicate the "/jcr:system" content in each workspace + and ensure those copies stay in sync. Federation is better than duplication. + + + By default, ModeShape creates this separate system workspace in a transient, in-memory store. This works great for some + simplistic cases, but this doesn't work when using clustering, + versioning, or dynamically registering namespaces or + adding or changing node types. + This is because these features all rely upon changing or adding content in the "/jcr:system" + area. For example, version histories are stored under "/jcr:system/jcr:versionStorage", node types + under "/jcr:system/jcr:versionStorage", and namespaces under "/jcr:system/mode:namespaces". + + + In these situations, it is necessary to persist the system content in a repository source, and if clustering is enabled + this source needs to be accessible to all members of the cluster. Many times, the easiest approach is to simply define + an extra workspace in your repository source where the system content can be stored. It's also possible to define + a separate repository source with a separate workspace for each repository's system content. (Using a separate source is required + when the repository is using a single repository source that can only store limited kinds of nodes, like the + file system connector or Subversion connector + that can only store nt:file and nt:folder nodes.) + + + You should always configure each ModeShape repository with a source for its system workspace by using the + SYSTEM_WORKSPACE_NAME repository option with a value that defines the name of source and name of the workspace + in that source where the system content should be stored, in the format: + + workspaceName@sourceName + + This specifies the system content should be stored in the workspace named "workspaceName" in the + "sourceName" repository source. + + + The system content can be stored in any repository source capable of storing any content and, in the case + of clustering, is accessible across multiple processes. For most people, this will mean a relational database. + Here is an abbreviated example of an XML configuration that defines a source for the system storage (in a MySQL database) + and a repository that uses it: + + + + + + + + + ... + + ... + + ... + + + + + + + + workspace1 + workspace2 + workspace3 + + ... + + ... + +]]> + + Of course, you can always use a separate workspace in your main source, too: + + + + + + + + + ... + + ... + + ... + + + + + workspace1 + workspace2 + workspace3 + system + + ... + + ... + +]]> + Clustering @@ -457,6 +584,12 @@ configuration.loadFrom(configSource); Note that the this example uses a child XML element for the "configuration", along with a CDATA section, so that the XML configuration can be nested within the ModeShape configuration. + + + Remember to specify the system workspace name + for each repository that is clustered. + + JGroups configuration Index: b/docs/reference/src/main/docbook/en-US/content/jcr/jcr.xml =================================================================== --- a/docs/reference/src/main/docbook/en-US/content/jcr/jcr.xml (revision ) +++ b/docs/reference/src/main/docbook/en-US/content/jcr/jcr.xml (working copy) @@ -168,6 +168,12 @@ try { options available when defining property definitions (e.g., searchable, queryable, etc.). Note that node type discovery is largely unchanged. + + + Remember to specify the system workspace name for your repositories + if dynamically adding or modifying node types. Otherwise, ModeShape will not persist your node type changes. + + Queries @@ -225,6 +231,12 @@ try { be aware that many of the locking-related methods on &Node; were deprecated in JCR 2.0 and moved to the new &LockManager; interface. However, locking semantics remain unchanged. + + + Remember to specify the system workspace name for your repositories + if clustering or if the lock information is to be persisted beyond the lifetime of the ModeShape engine. + + Versioning @@ -241,6 +253,12 @@ try { be aware that many of the version-related methods on &Node; were deprecated in JCR 2.0 and moved to the new &VersionManager; interface. Also, any reliance upon ModeShape's recursive restore operation must be changed, per the JCR 2.0 specification. + + + Remember to specify the system workspace name for your repositories + if using versioning. Otherwise, ModeShape will not persist your versioning information. + + Importing and Exporting Index: b/docs/reference/src/main/docbook/en-US/custom.dtd =================================================================== --- a/docs/reference/src/main/docbook/en-US/custom.dtd (revision ) +++ b/docs/reference/src/main/docbook/en-US/custom.dtd (working copy) @@ -231,6 +231,7 @@ JcrEngine"> JcrConfiguration"> JcrRepository"> +JcrRepository.Option"> JcrRepositoryFactory"> JcrSession"> JndiRepositoryFactory"> Index: b/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/BasicPath.java =================================================================== --- a/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/BasicPath.java (revision ) +++ b/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/BasicPath.java (working copy) @@ -23,6 +23,10 @@ */ package org.modeshape.graph.property.basic; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -52,9 +56,9 @@ public class BasicPath extends AbstractPath { public static final Path PARENT_PATH = new BasicPath(Collections.singletonList(Path.PARENT_SEGMENT), false); - private final List segments; - private final boolean absolute; - private final boolean normalized; + private/*final*/List segments; + private/*final*/boolean absolute; + private/*final*/boolean normalized; /** * @param segments the segments @@ -130,4 +134,30 @@ public class BasicPath extends AbstractPath { return this.segments.size(); } + /** + * Custom deserialization is needed, since the 'segments' list may not be serializable (e.g., java.util.RandomAccessSubList). + * + * @param aStream the input stream to which this object should be serialized; never null + * @throws IOException if there is a problem reading from the stream + * @throws ClassNotFoundException if there is a problem loading any required classes + */ + @SuppressWarnings( "unchecked" ) + private void readObject( ObjectInputStream aStream ) throws IOException, ClassNotFoundException { + absolute = aStream.readBoolean(); + normalized = aStream.readBoolean(); + segments = (List)aStream.readObject(); + } + + /** + * Custom serialization is needed, since the 'segments' list may not be serializable (e.g., java.util.RandomAccessSubList). + * + * @param aStream the input stream to which this object should be serialized; never null + * @throws IOException if there is a problem writing to the stream + */ + private void writeObject( ObjectOutputStream aStream ) throws IOException { + aStream.writeBoolean(absolute); + aStream.writeBoolean(normalized); + aStream.writeObject(Collections.unmodifiableList(new ArrayList(segments))); // make a copy! + } + } Index: b/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/GraphNamespaceRegistry.java =================================================================== --- a/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/GraphNamespaceRegistry.java (revision ) +++ b/modeshape-graph/src/main/java/org/modeshape/graph/property/basic/GraphNamespaceRegistry.java (working copy) @@ -30,10 +30,10 @@ import java.util.List; import java.util.Set; import net.jcip.annotations.NotThreadSafe; import org.modeshape.common.util.CheckArg; -import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Graph; import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Node; import org.modeshape.graph.Subgraph; import org.modeshape.graph.property.Name; @@ -94,6 +94,13 @@ public class GraphNamespaceRegistry implements NamespaceRegistry { } /** + * @return parentOfNamespaceNodes + */ + public Path getParentOfNamespaceNodes() { + return parentOfNamespaceNodes; + } + + /** * {@inheritDoc} */ public String getNamespaceForPrefix( String prefix ) { @@ -196,6 +203,9 @@ public class GraphNamespaceRegistry implements NamespaceRegistry { return cache.getNamespaces(); } + /** + * Refresh the namespaces from the persistent store, and update the embedded cache. This operation is done atomically. + */ public void refresh() { SimpleNamespaceRegistry newCache = new SimpleNamespaceRegistry(); initializeCacheFromStore(newCache); Index: b/modeshape-integration-tests/src/test/java/org/modeshape/test/integration/ClusteringTest.java =================================================================== --- a/modeshape-integration-tests/src/test/java/org/modeshape/test/integration/ClusteringTest.java (revision ) +++ b/modeshape-integration-tests/src/test/java/org/modeshape/test/integration/ClusteringTest.java (working copy) @@ -44,6 +44,7 @@ import javax.jcr.observation.EventListener; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.collection.Problem; import org.modeshape.common.util.FileUtil; import org.modeshape.connector.store.jpa.JpaSource; @@ -88,12 +89,15 @@ public class ClusteringTest { .setProperty("largeValueSizeInBytes", "150") .setProperty("autoGenerateSchema", "update") .setProperty("retryLimit", "3") - .setProperty("showSql", "false"); + .setProperty("showSql", "false") + .setProperty("defaultWorkspaceName", "content") + .setProperty("predefinedWorkspaceNames", new String[] {"content", "system"}); configuration.repository("cars") .setSource("car-source") .registerNamespace("car", "http://www.modeshape.org/examples/cars/1.0") .addNodeTypes(resourceUrl("cars.cnd")) - .setOption(Option.ANONYMOUS_USER_ROLES, ModeShapeRoles.ADMIN); + .setOption(Option.ANONYMOUS_USER_ROLES, ModeShapeRoles.ADMIN) + .setOption(Option.SYSTEM_SOURCE_NAME, "system@car-source"); configuration.clustering().setProperty("clusterName", "MyCluster");// .setProperty("configuration", ""); // Create an engine and use it to populate the source ... @@ -182,16 +186,20 @@ public class ClusteringTest { Session session2 = sessionFrom(engine2); Session session3 = sessionFrom(engine3); - int eventTypes = Event.NODE_ADDED | Event.NODE_REMOVED; // |Event.PROPERTY_ADDED|Event.PROPERTY_CHANGED|Event.PROPERTY_REMOVED - CustomListener listener1 = addListenerTo(session1, eventTypes, 1); - CustomListener listener2 = addListenerTo(session2, eventTypes, 1); - CustomListener listener3 = addListenerTo(session3, eventTypes, 1); - CustomListener remoteListener1 = addRemoteListenerTo(session1, eventTypes, 0); - CustomListener remoteListener2 = addRemoteListenerTo(session2, eventTypes, 1); - CustomListener remoteListener3 = addRemoteListenerTo(session2, eventTypes, 1); + // Make a place under which we can make our changes, because ADD events will be filtered by parent path ... + session1.getRootNode().addNode("Base"); + session1.save(); + + int eventTypes = Event.NODE_ADDED | Event.NODE_REMOVED; + CustomListener listener1 = addListenerTo(session1, null, eventTypes, 1); + CustomListener listener2 = addListenerTo(session2, "/Base", eventTypes, 1); + CustomListener listener3 = addListenerTo(session3, "/Base", eventTypes, 1); + CustomListener remoteListener1 = addRemoteListenerTo(session1, "/Base", eventTypes, 0); + CustomListener remoteListener2 = addRemoteListenerTo(session2, "/Base", eventTypes, 1); + CustomListener remoteListener3 = addRemoteListenerTo(session2, "/Base", eventTypes, 1); // Make some changes ... - session1.getRootNode().addNode("SomeNewNode"); + session1.getNode("/Base").addNode("SomeNewNode"); session1.save(); // Wait for all the listeners ... @@ -219,6 +227,74 @@ public class ClusteringTest { remoteListener3.checkObservedEvents(); } + @FixFor( "MODE-809" ) + @Test + public void shouldUpdateWorkspaceNamespaceRegistryAcrossCluster() throws Exception { + Session session1 = sessionFrom(engine1); + Session session2 = sessionFrom(engine2); + Session session3 = sessionFrom(engine3); + + // Register a new prefix in one session ... + String nsUri = "http://example.com/foo/bar/baz"; + String nsPrefix = "foo"; + session1.getWorkspace().getNamespaceRegistry().registerNamespace(nsPrefix, nsUri); + + // Make a place under which we can make our changes, because ADD events will be filtered by parent path ... + session1.getRootNode().addNode("Base"); + session1.save(); + + // Register some listeners for the workspace content changes we'll make shortly ... + int eventTypes = Event.NODE_ADDED | Event.NODE_REMOVED; + CustomListener listener1 = addListenerTo(session1, "/Base", eventTypes, 1); + CustomListener listener2 = addListenerTo(session2, "/Base", eventTypes, 1); + CustomListener listener3 = addListenerTo(session3, "/Base", eventTypes, 1); + CustomListener remoteListener1 = addRemoteListenerTo(session1, "/Base", eventTypes, 0); + CustomListener remoteListener2 = addRemoteListenerTo(session2, "/Base", eventTypes, 1); + CustomListener remoteListener3 = addRemoteListenerTo(session2, "/Base", eventTypes, 1); + + // Now create a node using this namespace ... + String propName = "foo:description"; + String propValue = "This is the foobar description"; + Node newNode = session1.getNode("/Base").addNode("SomeNewNode"); + newNode.setProperty(propName, propValue); + session1.save(); + final String path = newNode.getPath(); + + // Wait for all the listeners ... + listener1.await(); + listener2.await(); + listener3.await(); + remoteListener1.await(); + remoteListener2.await(); + remoteListener3.await(); + + // Disconnect the listeners ... + listener1.disconnect(); + listener2.disconnect(); + listener3.disconnect(); + remoteListener1.disconnect(); + remoteListener2.disconnect(); + remoteListener3.disconnect(); + + // Now check the events ... + listener1.checkObservedEvents(); + listener2.checkObservedEvents(); + listener3.checkObservedEvents(); + remoteListener1.checkObservedEvents(); + remoteListener2.checkObservedEvents(); + remoteListener3.checkObservedEvents(); + + // Check the namespace registry in each session ... + assertThat(session1.getWorkspace().getNamespaceRegistry().getURI(nsPrefix), is(nsUri)); + assertThat(session2.getWorkspace().getNamespaceRegistry().getURI(nsPrefix), is(nsUri)); + assertThat(session3.getWorkspace().getNamespaceRegistry().getURI(nsPrefix), is(nsUri)); + + // Now, load the node using the various sessions and verify the correct namespace registry has been used ... + assertThat(session1.getNode(path).getProperty(propName).getString(), is(propValue)); + assertThat(session2.getNode(path).getProperty(propName).getString(), is(propValue)); + assertThat(session3.getNode(path).getProperty(propName).getString(), is(propValue)); + } + // ---------------------------------------------------------------------------------------------------------------- // Utility Methods // ---------------------------------------------------------------------------------------------------------------- @@ -234,6 +310,7 @@ public class ClusteringTest { * Add a listener for only remote events. * * @param session the session + * @param absPath the absolute path at or below which the listener is interested in * @param eventTypes the type of events * @param expectedEventCount the number of expected events * @return the listener @@ -241,11 +318,12 @@ public class ClusteringTest { * @throws RepositoryException */ protected CustomListener addRemoteListenerTo( Session session, + String absPath, int eventTypes, int expectedEventCount ) throws UnsupportedRepositoryOperationException, RepositoryException { CustomListener listener = new CustomListener(session, expectedEventCount); - session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, null, true, null, null, true); + session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, absPath, true, null, null, true); return listener; } @@ -253,6 +331,8 @@ public class ClusteringTest { * Add a listener for local and remote events. * * @param session the session + * @param absPath the absolute path at or below which the listener is interested in, or null if the listener is interested in + * all nodes * @param eventTypes the type of events * @param expectedEventCount the number of expected events * @return the listener @@ -260,11 +340,12 @@ public class ClusteringTest { * @throws RepositoryException */ protected CustomListener addListenerTo( Session session, + String absPath, int eventTypes, int expectedEventCount ) throws UnsupportedRepositoryOperationException, RepositoryException { CustomListener listener = new CustomListener(session, expectedEventCount); - session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, null, true, null, null, false); + session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, absPath, true, null, null, false); return listener; } Index: b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java =================================================================== --- a/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (revision ) +++ b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (working copy) @@ -30,6 +30,7 @@ import java.security.AccessControlContext; import java.security.AccessControlException; import java.security.AccessController; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; @@ -411,7 +412,7 @@ public class JcrRepository implements Repository { private final String systemWorkspaceName; private final Projection systemSourceProjection; private final FederatedRepositorySource federatedSource; - private final NamespaceRegistry persistentRegistry; + private final GraphNamespaceRegistry persistentRegistry; private final RepositoryObservationManager repositoryObservationManager; private final SecurityContext anonymousUserContext; private final QueryParsers queryParsers; @@ -542,7 +543,7 @@ public class JcrRepository implements Repository { Name uriProperty = ModeShapeLexicon.URI; PathFactory pathFactory = executionContext.getValueFactories().getPathFactory(); Path systemPath = pathFactory.create(JcrLexicon.SYSTEM); - Path namespacesPath = pathFactory.create(systemPath, ModeShapeLexicon.NAMESPACES); + final Path namespacesPath = pathFactory.create(systemPath, ModeShapeLexicon.NAMESPACES); PropertyFactory propertyFactory = executionContext.getPropertyFactory(); Property namespaceType = propertyFactory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.NAMESPACE); @@ -703,7 +704,32 @@ public class JcrRepository implements Repository { repositoryLockManager = new RepositoryLockManager(this); - this.jcrSystemObservers = Collections.singletonList(repositoryLockManager); + // Create a system observer to update the namespace registry cache ... + final GraphNamespaceRegistry persistentRegistry = this.persistentRegistry; + final JcrSystemObserver namespaceObserver = new JcrSystemObserver() { + /** + * {@inheritDoc} + * + * @see org.modeshape.jcr.JcrSystemObserver#getObservedPath() + */ + public Path getObservedPath() { + return namespacesPath; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.observe.Observer#notify(org.modeshape.graph.observe.Changes) + */ + public void notify( Changes changes ) { + // These changes apply to anything at or below the namespaces path ... + persistentRegistry.refresh(); + } + }; + + // Define the set of "/jcr:system" observers ... + this.jcrSystemObservers = Collections.unmodifiableList(Arrays.asList(new JcrSystemObserver[] {repositoryLockManager, + namespaceObserver})); // This observer picks up notification of changes to the system graph in a cluster. It's a NOP if there is no cluster. this.repositoryObservationManager.register(new SystemChangeObserver()); @@ -1640,10 +1666,6 @@ public class JcrRepository implements Repository { // These are changes made locally by this repository ... return changes; } - if (systemSourceName.equals(changedSourceName)) { - // These are changes made locally by this repository ... - return changes; - } if (repositorySourceName.equals(changedSourceName)) { // These may be events generated locally or from a remote engine in the cluster ... if (this.processId.equals(changes.getProcessId())) { @@ -1656,6 +1678,11 @@ public class JcrRepository implements Repository { return new Changes(changes.getProcessId(), changes.getContextId(), changes.getUserName(), sourceName, changes.getTimestamp(), changes.getChangeRequests(), changes.getData()); } + assert !changedSourceName.equals(repositorySourceName); + if (systemSourceName.equals(changedSourceName)) { + // These are changes made locally by this repository ... + return changes; + } return null; } @@ -1731,7 +1758,7 @@ public class JcrRepository implements Repository { if (changedPath == null) continue; for (JcrSystemObserver jcrSystemObserver : getSystemObservers()) { - if (changedPath.isAtOrAbove(jcrSystemObserver.getObservedRootPath())) { + if (changedPath.isAtOrBelow(jcrSystemObserver.getObservedPath())) { systemChanges.put(jcrSystemObserver, change); } } Index: b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSystemObserver.java =================================================================== --- a/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSystemObserver.java (revision ) +++ b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSystemObserver.java (working copy) @@ -3,8 +3,16 @@ package org.modeshape.jcr; import org.modeshape.graph.observe.Observer; import org.modeshape.graph.property.Path; -public interface JcrSystemObserver extends Observer { +/** + * An interface for observers of the "/jcr:system" content. + */ +interface JcrSystemObserver extends Observer { - Path getObservedRootPath(); + /** + * Get the (absolute) path in the "/jcr:system" subgraph below which this observer is interested in changes. + * + * @return the observed path in the system content; may not be null + */ + Path getObservedPath(); } Index: b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java =================================================================== --- a/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (revision ) +++ b/modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (working copy) @@ -170,6 +170,10 @@ class JcrWorkspace implements Workspace { LocalNamespaceRegistry localRegistry = new LocalNamespaceRegistry(globalRegistry); this.context = context.with(localRegistry); + // Pre-cache all of the namespaces to be a snapshot of what's in the global registry at this time. + // This behavior is specified in Section 3.5.2 of the JCR 2.0 specification. + localRegistry.getNamespaces(); + // Now create a graph for the session ... this.graph = this.repository.createWorkspaceGraph(this.name, this.context); Index: b/modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryLockManager.java =================================================================== --- a/modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryLockManager.java (revision ) +++ b/modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryLockManager.java (working copy) @@ -118,7 +118,8 @@ class RepositoryLockManager implements JcrSystemObserver { for (Location lockLocation : locksGraph.getRoot().getChildren()) { Node lockNode = locksGraph.getNode(lockLocation); - Boolean isSessionScoped = booleanFactory.create(lockNode.getProperty(ModeShapeLexicon.IS_SESSION_SCOPED).getFirstValue()); + Boolean isSessionScoped = booleanFactory.create(lockNode.getProperty(ModeShapeLexicon.IS_SESSION_SCOPED) + .getFirstValue()); if (!isSessionScoped) continue; String lockingSession = stringFactory.create(lockNode.getProperty(ModeShapeLexicon.LOCKING_SESSION).getFirstValue()); @@ -127,7 +128,8 @@ class RepositoryLockManager implements JcrSystemObserver { if (activeSessionIds.contains(lockingSession)) { systemGraph.set(ModeShapeLexicon.EXPIRATION_DATE).on(lockLocation).to(newExpirationDate); } else { - DateTime expirationDate = dateFactory.create(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE).getFirstValue()); + DateTime expirationDate = dateFactory.create(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE) + .getFirstValue()); // Destroy expired locks (if it was still held by an active session, it would have been extended by now) if (expirationDate.isBefore(now)) { String workspaceName = stringFactory.create(lockNode.getProperty(ModeShapeLexicon.WORKSPACE).getFirstValue()); @@ -142,8 +144,13 @@ class RepositoryLockManager implements JcrSystemObserver { } } + /** + * {@inheritDoc} + * + * @see org.modeshape.jcr.JcrSystemObserver#getObservedPath() + */ @Override - public Path getObservedRootPath() { + public Path getObservedPath() { return locksPath; } @@ -153,6 +160,10 @@ class RepositoryLockManager implements JcrSystemObserver { assert change.changedLocation().hasPath(); Path changedPath = change.changedLocation().getPath(); + if (changedPath.equals(locksPath)) { + // nothing to do with the "/jcr:system/mode:locks" node ... + continue; + } assert locksPath.isAncestorOf(changedPath); Segment rawUuid = changedPath.getLastSegment(); @@ -165,35 +176,32 @@ class RepositoryLockManager implements JcrSystemObserver { switch (change.getType()) { case CREATE_NODE: - CreateNodeRequest create = (CreateNodeRequest) change; - - Property lockOwnerProp= null; + CreateNodeRequest create = (CreateNodeRequest)change; + + Property lockOwnerProp = null; Property lockUuidProp = null; Property isDeepProp = null; Property isSessionScopedProp = null; - + for (Property prop : create.properties()) { if (JcrLexicon.LOCK_OWNER.equals(prop.getName())) { lockOwnerProp = prop; - } - else if (JcrLexicon.LOCK_IS_DEEP.equals(prop.getName())) { + } else if (JcrLexicon.LOCK_IS_DEEP.equals(prop.getName())) { isDeepProp = prop; - } - else if (ModeShapeLexicon.IS_HELD_BY_SESSION.equals(prop.getName())) { + } else if (ModeShapeLexicon.IS_HELD_BY_SESSION.equals(prop.getName())) { isSessionScopedProp = prop; - } - else if (JcrLexicon.UUID.equals(prop.getName())) { + } else if (JcrLexicon.UUID.equals(prop.getName())) { isSessionScopedProp = prop; } } - + String lockOwner = firstString(lockOwnerProp); UUID lockUuid = firstUuid(lockUuidProp); boolean isDeep = firstBoolean(isDeepProp); boolean isSessionScoped = firstBoolean(isSessionScopedProp); workspaceManager.lockNodeInternally(lockOwner, lockUuid, lockedNodeUuid, isDeep, isSessionScoped); - + break; case DELETE_BRANCH: boolean success = workspaceManager.unlockNodeInternally(lockedNodeUuid); @@ -202,37 +210,37 @@ class RepositoryLockManager implements JcrSystemObserver { break; default: - assert false :"Unexpected change request: " + change; + assert false : "Unexpected change request: " + change; } - + } } - private final String string(Segment rawString) { + private final String string( Segment rawString ) { ExecutionContext context = repository.getExecutionContext(); return context.getValueFactories().getStringFactory().create(rawString); } - private final String firstString(Property property) { + private final String firstString( Property property ) { if (property == null) return null; Object firstValue = property.getFirstValue(); - + ExecutionContext context = repository.getExecutionContext(); return context.getValueFactories().getStringFactory().create(firstValue); } - private final UUID firstUuid(Property property) { + private final UUID firstUuid( Property property ) { if (property == null) return null; Object firstValue = property.getFirstValue(); - + ExecutionContext context = repository.getExecutionContext(); return context.getValueFactories().getUuidFactory().create(firstValue); } - - private final boolean firstBoolean(Property property) { + + private final boolean firstBoolean( Property property ) { if (property == null) return false; Object firstValue = property.getFirstValue(); - + ExecutionContext context = repository.getExecutionContext(); return context.getValueFactories().getBooleanFactory().create(firstValue); }