Package org.jboss.dna.jcr

Source Code of org.jboss.dna.jcr.WorkspaceLockManager

package org.jboss.dna.jcr;

import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import net.jcip.annotations.ThreadSafe;
import org.jboss.dna.graph.ExecutionContext;
import org.jboss.dna.graph.Graph;
import org.jboss.dna.graph.Location;
import org.jboss.dna.graph.connector.LockFailedException;
import org.jboss.dna.graph.property.DateTime;
import org.jboss.dna.graph.property.DateTimeFactory;
import org.jboss.dna.graph.property.Path;
import org.jboss.dna.graph.property.PathFactory;
import org.jboss.dna.graph.property.PathNotFoundException;
import org.jboss.dna.graph.property.Property;
import org.jboss.dna.graph.property.PropertyFactory;
import org.jboss.dna.graph.property.ValueFactory;
import org.jboss.dna.jcr.SessionCache.NodeEditor;

/**
* Manages the locks for a particular workspace in a repository. Locks are stored in a {@code Map<UUID, DnaLock>} while they exist
* and are discarded after they are discarded (through the {@link Node#unlock()} method).
*/
@ThreadSafe
class WorkspaceLockManager {

    private final ExecutionContext context;
    private final Path locksPath;
    private final JcrRepository repository;
    private final String workspaceName;
    private final ConcurrentMap<UUID, DnaLock> workspaceLocksByNodeUuid;

    WorkspaceLockManager( ExecutionContext context,
                          JcrRepository repository,
                          String workspaceName,
                          Path locksPath ) {
        this.context = context;
        this.repository = repository;
        this.workspaceName = workspaceName;
        this.locksPath = locksPath;

        this.workspaceLocksByNodeUuid = new ConcurrentHashMap<UUID, DnaLock>();

        Property locksPrimaryType = context.getPropertyFactory().create(JcrLexicon.PRIMARY_TYPE, DnaLexicon.LOCKS);
        repository.createSystemGraph(context).create(locksPath, locksPrimaryType).ifAbsent().and();
    }

    /**
     * Creates a lock on the node with the given {@link Location}. This method creates a new lock, registers it in the list of
     * locks, immediately modifies the {@code jcr:lockOwner} and {@code jcr:lockIsDeep} properties on the node in the underlying
     * repository, and adds the lock to the system view.
     * <p>
     * The location given in {@code nodeLocation} must have a UUID.
     * </p>
     *
     * @param session the session in which the node is being locked and that loaded the node
     * @param nodeLocation the location for the node; may not be null and must have a UUID
     * @param isDeep whether the node's descendants in the content graph should also be locked
     * @param isSessionScoped whether the lock should outlive the session in which it was created
     * @return an object representing the newly created lock
     * @throws RepositoryException if an error occurs updating the graph state
     */
    DnaLock lock( JcrSession session,
                  Location nodeLocation,
                  boolean isDeep,
                  boolean isSessionScoped ) throws RepositoryException {
        assert nodeLocation != null;

        UUID lockUuid = UUID.randomUUID();
        UUID nodeUuid = uuidFor(session, nodeLocation);

        if (nodeUuid == null) {
            throw new RepositoryException(JcrI18n.uuidRequiredForLock.text(nodeLocation));
        }

        ExecutionContext sessionContext = session.getExecutionContext();
        String lockOwner = session.getUserID();
        DnaLock lock = createLock(lockOwner, lockUuid, nodeUuid, isDeep, isSessionScoped);

        Graph.Batch batch = repository.createSystemGraph(sessionContext).batch();

        PropertyFactory propFactory = sessionContext.getPropertyFactory();
        PathFactory pathFactory = sessionContext.getValueFactories().getPathFactory();
        Property lockOwnerProp = propFactory.create(JcrLexicon.LOCK_OWNER, lockOwner);
        Property lockIsDeepProp = propFactory.create(JcrLexicon.LOCK_IS_DEEP, isDeep);

        DateTimeFactory dateFactory = sessionContext.getValueFactories().getDateFactory();
        DateTime expirationDate = dateFactory.create();
        expirationDate = expirationDate.plusMillis(JcrEngine.LOCK_EXTENSION_INTERVAL_IN_MILLIS);

        batch.create(pathFactory.create(locksPath, pathFactory.createSegment(lockUuid.toString())),
                     propFactory.create(JcrLexicon.PRIMARY_TYPE, DnaLexicon.LOCK),
                     propFactory.create(DnaLexicon.WORKSPACE, workspaceName),
                     propFactory.create(DnaLexicon.LOCKED_UUID, nodeUuid.toString()),
                     propFactory.create(DnaLexicon.IS_SESSION_SCOPED, isSessionScoped),
                     propFactory.create(DnaLexicon.LOCKING_SESSION, session.sessionId()),
                     propFactory.create(DnaLexicon.EXPIRATION_DATE, expirationDate),
                     // This gets set after the lock succeeds and the lock token gets added to the session
                     propFactory.create(DnaLexicon.IS_HELD_BY_SESSION, false),
                     lockOwnerProp,
                     lockIsDeepProp).ifAbsent().and();
        batch.execute();

        SessionCache cache = session.cache();
        AbstractJcrNode lockedNode = cache.findJcrNode(Location.create(nodeUuid));
        NodeEditor editor = cache.getEditorFor(lockedNode.nodeInfo());

        // Set the properties in the cache...
        editor.setProperty(JcrLexicon.LOCK_OWNER,
                           (JcrValue)cache.session().getValueFactory().createValue(lockOwner, PropertyType.STRING),
                           false);
        editor.setProperty(JcrLexicon.LOCK_IS_DEEP, (JcrValue)cache.session().getValueFactory().createValue(isDeep), false);

        lockNodeInRepository(session, nodeUuid, lockOwnerProp, lockIsDeepProp, lock, isDeep);
        workspaceLocksByNodeUuid.put(nodeUuid, lock);

        return lock;
    }

    DnaLock createLock( org.jboss.dna.graph.Node lockNode ) {
        return new DnaLock(lockNode);
    }

    /* Factory method added to facilitate mocked testing */
    DnaLock createLock( String lockOwner,
                        UUID lockUuid,
                        UUID nodeUuid,
                        boolean isDeep,
                        boolean isSessionScoped ) {
        return new DnaLock(lockOwner, lockUuid, nodeUuid, isDeep, isSessionScoped);
    }

    /**
     * Marks the node as locked in the underlying repository with an immediate write (that is, the write is not part of the JCR
     * session scope and cannot be "rolled back" with a refresh at the {@link Session#refresh(boolean) session} or
     * {@link Item#refresh(boolean) item} level.
     * <p>
     * This method will also attempt to {@link Graph#lock(Location) lock the node in the underlying repository}. If the underlying
     * repository supports locks and {@link LockFailedException the lock attempt fails}, this method will cancel the lock attempt
     * by calling {@link #unlock(ExecutionContext, DnaLock)} and will throw a {@code RepositoryException}.
     * </p>
     * <p>
     * This method does not modify the system graph. In other words, it will not create the record for the lock in the {@code
     * /jcr:system/dna:locks} subgraph.
     * </p>
     *
     * @param session the session in which the node is being locked and that loaded the node
     * @param nodeUuid the UUID of the node to lock
     * @param lockOwnerProp an existing property with name {@link JcrLexicon#LOCK_OWNER} and the value being the name of the lock
     *        owner
     * @param lockIsDeepProp an existing property with name {@link JcrLexicon#LOCK_IS_DEEP} and the value being {@code true} if
     *        the lock should include all descendants of the locked node or {@code false} if the lock should only include the
     *        specified node and not its descendants
     * @param lock the internal lock representation
     * @param isDeep {@code true} if the lock should include all descendants of the locked node or {@code false} if the lock
     *        should only include the specified node and not its descendants. This value is redundant with the lockIsDeep
     *        parameter, but is included separately as a minor performance optimization
     * @throws RepositoryException if the repository in which the node represented by {@code nodeUuid} supports locking but
     *         signals that the lock for the node cannot be acquired
     */
    void lockNodeInRepository( JcrSession session,
                               UUID nodeUuid,
                               Property lockOwnerProp,
                               Property lockIsDeepProp,
                               DnaLock lock,
                               boolean isDeep ) throws RepositoryException {
        // Write them directly to the underlying graph
        Graph.Batch workspaceBatch = repository.createWorkspaceGraph(workspaceName, session.getExecutionContext()).batch();
        workspaceBatch.set(lockOwnerProp, lockIsDeepProp).on(nodeUuid);
        if (isDeep) {
            workspaceBatch.lock(nodeUuid).andItsDescendants().withDefaultTimeout();
        } else {
            workspaceBatch.lock(nodeUuid).only().withDefaultTimeout();
        }
        try {
            workspaceBatch.execute();
        } catch (LockFailedException lfe) {
            // Attempt to lock node at the repo level failed - cancel lock
            unlock(session.getExecutionContext(), lock);
            throw new RepositoryException(lfe);
        }

    }

    /**
     * Removes the provided lock, effectively unlocking the node to which the lock is associated.
     *
     * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
     * @param lock the lock to be removed
     */
    void unlock( ExecutionContext sessionExecutionContext,
                 DnaLock lock ) {
        try {
            PathFactory pathFactory = sessionExecutionContext.getValueFactories().getPathFactory();

            // Remove the lock node under the /jcr:system branch ...
            Graph.Batch batch = repository.createSystemGraph(sessionExecutionContext).batch();
            batch.delete(pathFactory.create(locksPath, pathFactory.createSegment(lock.getUuid().toString())));
            batch.execute();

            // Unlock the node in the repository graph ...
            unlockNodeInRepository(sessionExecutionContext, lock);

            workspaceLocksByNodeUuid.remove(lock.nodeUuid);
        } catch (PathNotFoundException pnfe) {
            /*
             * This can legitimately happen if there is a session-scoped lock on a node which is then deleted by the lock owner and the lock
             * owner logs out.  At that point, the lock owner still holds the token for the lock, so it will get cleaned up with a call to cleanLocks.
             */
            if (!lock.nodeUuid.equals(pnfe.getLocation().getUuid())) {
                // If the lock node under dna:locks is not found, this is an internal error
                throw new IllegalStateException(pnfe);
            }
            workspaceLocksByNodeUuid.remove(lock.nodeUuid);
        }
    }

    /**
     * Removes the workspace record of the lock in the underlying repository. This method clears the {@code jcr:lockOwner} and
     * {@code jcr:lockIsDeep} properties on the node and sends an {@link Graph#unlock(Location) unlock request} to the underlying
     * repository to clear any locks that it is holding on the node.
     * <p>
     * This method does not modify the system graph. In other words, it will not remove the record for the lock in the {@code
     * /jcr:system/dna:locks} subgraph.
     * </p>
     *
     * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
     * @param lock
     */
    void unlockNodeInRepository( ExecutionContext sessionExecutionContext,
                                 DnaLock lock ) {
        Graph.Batch workspaceBatch = repository.createWorkspaceGraph(this.workspaceName, sessionExecutionContext).batch();

        workspaceBatch.remove(JcrLexicon.LOCK_OWNER, JcrLexicon.LOCK_IS_DEEP).on(lock.nodeUuid);
        workspaceBatch.unlock(lock.nodeUuid);

        workspaceBatch.execute();
    }

    /**
     * Checks whether the given lock token is currently held by any session by querying the lock record in the underlying
     * repository.
     *
     * @param session the session on behalf of which the lock query is being performed
     * @param lockToken the lock token to check; may not be null
     * @return true if a session currently holds the lock token, false otherwise
     */
    boolean isHeldBySession( JcrSession session,
                             String lockToken ) {
        assert lockToken != null;

        ExecutionContext context = session.getExecutionContext();
        ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();
        PathFactory pathFactory = context.getValueFactories().getPathFactory();

        org.jboss.dna.graph.Node lockNode = repository.createSystemGraph(context)
                                                      .getNodeAt(pathFactory.create(locksPath,
                                                                                    pathFactory.createSegment(lockToken)));

        return booleanFactory.create(lockNode.getProperty(DnaLexicon.IS_HELD_BY_SESSION).getFirstValue());

    }

    /**
     * Updates the underlying repository directly (i.e., outside the scope of the {@link Session}) to mark the token for the given
     * lock as being held (or not held) by some {@link Session}. Note that this method does not identify <i>which</i> (if any)
     * session holds the token for the lock, just that <i>some</i> session holds the token for the lock.
     *
     * @param session the session on behalf of which the lock operation is being performed
     * @param lockToken the lock token for which the "held" status should be modified; may not be null
     * @param value the new value
     */
    void setHeldBySession( JcrSession session,
                           String lockToken,
                           boolean value ) {
        assert lockToken != null;

        ExecutionContext context = session.getExecutionContext();
        PropertyFactory propFactory = context.getPropertyFactory();
        PathFactory pathFactory = context.getValueFactories().getPathFactory();

        repository.createSystemGraph(context)
                  .set(propFactory.create(DnaLexicon.IS_HELD_BY_SESSION, value))
                  .on(pathFactory.create(locksPath, pathFactory.createSegment(lockToken)));
    }

    /**
     * Returns the lock that corresponds to the given lock token
     *
     * @param lockToken the lock token
     * @return the corresponding lock, possibly null
     * @see Session#addLockToken(String)
     * @see Session#removeLockToken(String)
     */
    DnaLock lockFor( String lockToken ) {
        for (DnaLock lock : workspaceLocksByNodeUuid.values()) {
            if (lockToken.equals(lock.getLockToken())) {
                return lock;
            }
        }

        return null;
    }

    /**
     * Returns the lock that corresponds to the given UUID
     *
     * @param session the session on behalf of which the lock operation is being performed
     * @param nodeLocation the node UUID
     * @return the corresponding lock, possibly null if there is no such lock
     */
    DnaLock lockFor( JcrSession session,
                     Location nodeLocation ) {
        UUID nodeUuid = uuidFor(session, nodeLocation);
        if (nodeUuid == null) return null;
        return workspaceLocksByNodeUuid.get(nodeUuid);
    }

    /**
     * Returns the UUID that identifies the given location or {@code null} if the location does not have a UUID. The method
     * returns the {@link Location#getUuid() default UUID} if it exists. If it does not, the method returns the value of the
     * {@link JcrLexicon#UUID} property as a UUID. If the location does not contain that property, the method returns null.
     *
     * @param session the session on behalf of which the lock operation is being performed
     * @param location the location for which the UUID should be returned
     * @return the UUID that identifies the given location or {@code null} if the location does not have a UUID.
     */
    UUID uuidFor( JcrSession session,
                  Location location ) {
        assert location != null;

        if (location.getUuid() != null) return location.getUuid();

        org.jboss.dna.graph.property.Property uuidProp = location.getIdProperty(JcrLexicon.UUID);
        if (uuidProp == null) return null;

        ExecutionContext context = session.getExecutionContext();
        return context.getValueFactories().getUuidFactory().create(uuidProp.getFirstValue());
    }

    /**
     * Unlocks all locks corresponding to the tokens in the {@code lockTokens} collection that are session scoped.
     *
     * @param session the session on behalf of which the lock operation is being performed
     */
    void cleanLocks( JcrSession session ) {
        ExecutionContext context = session.getExecutionContext();
        Collection<String> lockTokens = session.lockTokens();
        for (String lockToken : lockTokens) {
            DnaLock lock = lockFor(lockToken);
            if (lock != null && lock.isSessionScoped()) {
                unlock(context, lock);
            }
        }
    }

    /**
     * Internal representation of a locked node. This class should only be created through calls to
     * {@link WorkspaceLockManager#lock(JcrSession, Location, boolean, boolean)}.
     */
    @ThreadSafe
    public class DnaLock {
        final UUID nodeUuid;
        private final UUID lockUuid;
        private final String lockOwner;
        private final boolean deep;
        private final boolean sessionScoped;

        @SuppressWarnings( "synthetic-access" )
        DnaLock( org.jboss.dna.graph.Node lockNode ) {
            ValueFactory<String> stringFactory = context.getValueFactories().getStringFactory();
            ValueFactory<UUID> uuidFactory = context.getValueFactories().getUuidFactory();
            ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();

            assert lockNode.getLocation().getPath() != null;

            String lockUuidAsString = lockNode.getLocation().getPath().getLastSegment().getName().getLocalName();
            Property lockOwnerProperty = lockNode.getProperty(JcrLexicon.LOCK_OWNER);
            Property nodeUuidProperty = lockNode.getProperty(DnaLexicon.LOCKED_UUID);
            Property lockIsDeepProperty = lockNode.getProperty(JcrLexicon.LOCK_IS_DEEP);
            Property isSessionScopedProperty = lockNode.getProperty(DnaLexicon.IS_SESSION_SCOPED);

            assert lockUuidAsString != null;
            assert lockOwnerProperty != null;
            assert nodeUuidProperty != null;
            assert lockIsDeepProperty != null;
            assert isSessionScopedProperty != null;

            this.lockOwner = stringFactory.create(lockOwnerProperty.getFirstValue());
            this.lockUuid = UUID.fromString(lockUuidAsString);
            this.nodeUuid = uuidFactory.create(nodeUuidProperty.getFirstValue());
            this.deep = booleanFactory.create(lockIsDeepProperty.getFirstValue());
            this.sessionScoped = booleanFactory.create(isSessionScopedProperty.getFirstValue());
        }

        DnaLock( String lockOwner,
                 UUID lockUuid,
                 UUID nodeUuid,
                 boolean deep,
                 boolean sessionScoped ) {
            super();
            this.lockOwner = lockOwner;
            this.lockUuid = lockUuid;
            this.nodeUuid = nodeUuid;
            this.deep = deep;
            this.sessionScoped = sessionScoped;
        }

        @SuppressWarnings( "synthetic-access" )
        public boolean isLive() {
            return workspaceLocksByNodeUuid.containsKey(nodeUuid);
        }

        public UUID getUuid() {
            return lockUuid;
        }

        public boolean isDeep() {
            return deep;
        }

        public String getLockOwner() {
            return lockOwner;
        }

        public boolean isSessionScoped() {
            return sessionScoped;
        }

        public String getLockToken() {
            return lockUuid.toString();
        }

        @SuppressWarnings( "synthetic-access" )
        public Lock lockFor( SessionCache cache ) throws RepositoryException {
            final AbstractJcrNode node = cache.findJcrNode(Location.create(nodeUuid));
            final JcrSession session = cache.session();
            return new Lock() {
                public String getLockOwner() {
                    return lockOwner;
                }

                public String getLockToken() {
                    String uuidString = lockUuid.toString();
                    return session.lockTokens().contains(uuidString) ? uuidString : null;
                }

                public Node getNode() {
                    return node;
                }

                public boolean isDeep() {
                    return deep;
                }

                public boolean isLive() {
                    return workspaceLocksByNodeUuid.containsKey(nodeUuid);
                }

                public boolean isSessionScoped() {
                    return sessionScoped;
                }

                public void refresh() throws LockException {
                    if (getLockToken() == null) {
                        throw new LockException(JcrI18n.notLocked.text(node.location));
                    }
                }
            };
        }

    }
}
TOP

Related Classes of org.jboss.dna.jcr.WorkspaceLockManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.