/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.lock.LockException; import org.modeshape.common.annotation.Immutable; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.common.logging.Logger; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.CachedNode; import org.modeshape.jcr.cache.ChildReference; import org.modeshape.jcr.cache.ChildReferences; import org.modeshape.jcr.cache.DocumentAlreadyExistsException; import org.modeshape.jcr.cache.LockFailureException; import org.modeshape.jcr.cache.MutableCachedNode; import org.modeshape.jcr.cache.NodeCache; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.SessionCache; import org.modeshape.jcr.cache.change.Change; import org.modeshape.jcr.cache.change.ChangeSet; import org.modeshape.jcr.cache.change.ChangeSetListener; import org.modeshape.jcr.cache.change.NodeAdded; import org.modeshape.jcr.cache.change.NodeRemoved; import org.modeshape.jcr.value.DateTimeFactory; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.PathFactory; import org.modeshape.jcr.value.Property; import org.modeshape.jcr.value.PropertyFactory; @ThreadSafe class RepositoryLockManager implements ChangeSetListener { private static final int KEY_OFFSET = "mode:lock-".length(); private final JcrRepository.RunningState repository; private final String systemWorkspaceName; private final String processId; private final ConcurrentMap<NodeKey, ModeShapeLock> locksByNodeKey; private final Path locksPath; private final Logger logger; private final long lockExtensionIntervalMillis; private final long defaultLockAgeMillis; RepositoryLockManager( JcrRepository.RunningState repository, RepositoryConfiguration.GarbageCollection gcConfig ) { this.repository = repository; this.systemWorkspaceName = repository.repositoryCache().getSystemWorkspaceName(); this.processId = repository.context().getProcessId(); this.locksByNodeKey = new ConcurrentHashMap<NodeKey, ModeShapeLock>(); PathFactory pathFactory = repository.context().getValueFactories().getPathFactory(); this.locksPath = pathFactory.create(pathFactory.createRootPath(), JcrLexicon.SYSTEM, ModeShapeLexicon.LOCKS); this.logger = Logger.getLogger(getClass()); long lockGCIntervalMillis = gcConfig.getIntervalInMillis(); assert lockGCIntervalMillis > 0; /* * Each time the garbage collection process runs, session-scoped locks that are still used by active sessions will have their * expiry times extended by this amount of time. Each repository instance in the ModeShape cluster will run its own cleanup * process, which will extend the expiry times of its own locks. As soon as a repository is no longer running the cleanup * process, we know that there can be no active sessions. * * The default GC interval is expressed in hours, so we make the extension slightly larger to avoid any lock being expired * prematurely. */ this.lockExtensionIntervalMillis = lockGCIntervalMillis + TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES); /* * The amount of time that a lock may be active before considered expired. The sweep process will extend the locks for active * sessions, so only unused locks will have an unmodified expiry time. * * The default GC interval is expressed in hours, so we make the default age slightly less to avoid stale lock. */ this.defaultLockAgeMillis = lockGCIntervalMillis - TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES); } RepositoryLockManager with( JcrRepository.RunningState repository, RepositoryConfiguration.GarbageCollection gcConfig ) { assert this.systemWorkspaceName == repository.repositoryCache().getSystemWorkspaceName(); assert this.processId == repository.context().getProcessId(); PathFactory pathFactory = repository.context().getValueFactories().getPathFactory(); Path locksPath = pathFactory.create(pathFactory.createRootPath(), JcrLexicon.SYSTEM, ModeShapeLexicon.LOCKS); assert this.locksPath.equals(locksPath); return new RepositoryLockManager(repository, gcConfig); } /** * Refresh the locks from the stored representation. */ protected void refreshFromSystem() { try { // Re-read and re-register all of the namespaces ... SessionCache systemCache = repository.createSystemSession(repository.context(), false); SystemContent system = new SystemContent(systemCache); CachedNode locks = system.locksNode(); MutableCachedNode mutableLocks = null; Set<NodeKey> corruptedLocks = new HashSet<>(); for (ChildReference ref : locks.getChildReferences(systemCache)) { CachedNode node = systemCache.getNode(ref); if (node == null) { if (mutableLocks == null) { mutableLocks = system.mutableLocksNode(); } NodeKey lockKey = ref.getKey(); logger.warn(JcrI18n.lockNotFound, lockKey); mutableLocks.removeChild(systemCache, lockKey); corruptedLocks.add(lockKey); continue; } ModeShapeLock lock = new ModeShapeLock(node, systemCache); locksByNodeKey.put(lock.getLockedNodeKey(), lock); } if (mutableLocks != null) { system.save(); for (Iterator<Map.Entry<NodeKey, ModeShapeLock>> locksIterator = locksByNodeKey.entrySet().iterator(); locksIterator.hasNext();) { NodeKey lockKey = locksIterator.next().getValue().getLockKey(); if (corruptedLocks.contains(lockKey)) { locksIterator.remove(); } } } } catch (Throwable e) { logger.error(e, JcrI18n.errorRefreshingLocks, repository.name()); } } /** * Clean up the locks within the repository's system content. Any locks held by active sessions are extended/renewed, while * those locks that are significantly expired are removed. * * @param activeSessionIds the IDs of the sessions that are still active in this repository * @throws TimeoutException if a timeout occurs attempting to lock nodes */ protected void cleanupLocks( Set<String> activeSessionIds ) throws TimeoutException { try { ExecutionContext context = repository.context(); DateTimeFactory dates = context.getValueFactories().getDateFactory(); DateTime now = dates.create(); DateTime newExpiration = dates.create(now, this.lockExtensionIntervalMillis); PropertyFactory propertyFactory = context.getPropertyFactory(); SessionCache systemSession = repository.createSystemSession(context, false); SystemContent systemContent = new SystemContent(systemSession); Map<String, List<NodeKey>> lockedNodesByWorkspaceName = new HashMap<>(); // Iterate over the locks ... CachedNode locksNode = systemContent.locksNode(); ChildReferences childReferences = locksNode.getChildReferences(systemSession); if (childReferences.isEmpty()) { // there are no locks, so nothing to do return; } for (ChildReference ref : childReferences) { NodeKey lockKey = ref.getKey(); CachedNode lockNode = systemSession.getNode(lockKey); if (lockNode == null) { //it may happen that another thread has performed a session.logout which means the lock might have been removed continue; } ModeShapeLock lock = new ModeShapeLock(lockNode, systemSession); NodeKey lockedNodeKey = lock.getLockedNodeKey(); if (lock.isSessionScoped() && activeSessionIds.contains(lock.getLockingSessionId())) { //for active session locks belonging to the sessions of this process, we want to extend the expiration date //so that other processes in a cluster can tell that this lock is still active MutableCachedNode mutableLockNode = systemSession.mutable(lockKey); Property prop = propertyFactory.create(ModeShapeLexicon.EXPIRATION_DATE, newExpiration); mutableLockNode.setProperty(systemSession, prop); //reflect the change in the expiry date in the internal map this.locksByNodeKey.replace(lockedNodeKey, lock.withExpiryTime(newExpiration)); continue; } //if it's not an active session lock, we always check the expiry date DateTime expirationDate = firstDate(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE, systemSession)); if (expirationDate.isBefore(now)) { //remove the lock from the system area systemContent.removeLock(lock); //register the target node which needs cleaning List<NodeKey> lockedNodes = lockedNodesByWorkspaceName.get(lock.getWorkspaceName()); if (lockedNodes == null) { lockedNodes = new ArrayList<>(); lockedNodesByWorkspaceName.put(lock.getWorkspaceName(), lockedNodes); } lockedNodes.add(lockedNodeKey); } } //persist all the changes to the locks from the system area systemSession.save(); if (!lockedNodesByWorkspaceName.isEmpty()) { //update each of nodes which has been unlocked for (String workspaceName : lockedNodesByWorkspaceName.keySet()) { SessionCache internalSession = repository.repositoryCache().createSession(context, workspaceName, false); for (NodeKey lockedNodeKey : lockedNodesByWorkspaceName.get(workspaceName)) { //clear the internal cache this.locksByNodeKey.remove(lockedNodeKey); CachedNode lockedNode = internalSession.getWorkspace().getNode(lockedNodeKey); if (lockedNode != null) { MutableCachedNode mutableLockedNode = internalSession.mutable(lockedNodeKey); mutableLockedNode.removeProperty(internalSession, JcrLexicon.LOCK_IS_DEEP); mutableLockedNode.removeProperty(internalSession, JcrLexicon.LOCK_OWNER); mutableLockedNode.unlock(); } } internalSession.save(); } } } catch (TimeoutException te) { // there was a timeout while locking on some nodes (most likely mode:locks) so we should re-throw this to callers // so they can react throw te; } catch (Throwable t) { logger.error(t, JcrI18n.errorCleaningUpLocks, repository.name()); } } protected final NodeKey lockedNodeKeyFromLockKey( NodeKey key ) { // The identifier of the lock key contains "mode:lock-" followed by the full key of the locked node ... String identifier = key.getIdentifier(); return new NodeKey(identifier.substring(KEY_OFFSET)); } final boolean isLocked( NodeKey lockedNodeKey ) { return locksByNodeKey.containsKey(lockedNodeKey); } final ModeShapeLock findLockFor( NodeKey nodeKey ) { return locksByNodeKey.get(nodeKey); } private final CachedNode findLockedNodeAtOrBelow( CachedNode node, NodeCache cache ) { if (node.getChildReferences(cache).isEmpty()) { // It is a leaf node, so just check for a lock on the node ... if (isLocked(node.getKey())) return node; } // We assume that there are far fewer locks than there are descendants of the supplied path ... Path path = node.getPath(cache); for (ModeShapeLock lock : locksByNodeKey.values()) { CachedNode lockedNode = cache.getNode(lock.getLockedNodeKey()); if (lockedNode == null) continue; Path lockedPath = lockedNode.getPath(cache); if (lockedPath.isAtOrBelow(path)) return lockedNode; } return null; } final Collection<ModeShapeLock> allLocks() { return locksByNodeKey.values(); } protected final NodeKey generateLockKey( NodeKey prototype, NodeKey lockedNodeKey ) { return prototype.withId("mode:lock-" + lockedNodeKey.toString()); } protected final String generateLockToken() { return UUID.randomUUID().toString(); } /** * Creates a lock on the node with the given {@link NodeKey key}. 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 persisted node in the * underlying repository, and adds the lock to the persistent system view. * <p> * This method <i>assumes</i> that the node is lockable and not already locked. * </p> * * @param session the session in which the node is being locked and that loaded the node * @param node the node that is being locked; may not be null * @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 * @param timeoutHint desired lock timeout in seconds (servers are free to ignore this value); specify {@link Long#MAX_VALUE} * for no timeout. * @param ownerInfo a string containing owner information supplied by the client, and recorded on the lock; if null, then the * session's user ID will be used * @return an object representing the newly created lock * @throws LockException if the lock could not be obtained * @throws RepositoryException if an error occurs updating the graph state */ ModeShapeLock lock( JcrSession session, CachedNode node, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo ) throws LockException, RepositoryException { assert session != null; assert node != null; final ExecutionContext context = session.context(); final String owner = ownerInfo != null ? ownerInfo : session.getUserID(); final DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); long expirationTimeInMillis = this.defaultLockAgeMillis; if (timeoutHint > 0 && timeoutHint < Long.MAX_VALUE) { expirationTimeInMillis = TimeUnit.MILLISECONDS.convert(timeoutHint, TimeUnit.SECONDS); } DateTime expirationDate = dateFactory.create().plus(Duration.ofMillis(expirationTimeInMillis)); // Create a new lock ... SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); NodeKey nodeKey = node.getKey(); NodeKey lockKey = generateLockKey(system.locksKey(), nodeKey); String token = generateLockToken(); ModeShapeLock lock = new ModeShapeLock(nodeKey, lockKey, session.workspaceName(), owner, token, isDeep, isSessionScoped, session.sessionId(), expirationDate); if (isDeep) { NodeCache cache = session.cache(); CachedNode locked = findLockedNodeAtOrBelow(node, cache); if (locked != null) { String nodePath = session.stringFactory().create(node.getPath(cache)); String descendantPath = session.stringFactory().create(locked.getPath(cache)); throw new LockException(JcrI18n.descendantAlreadyLocked.text(nodePath, descendantPath)); } } ModeShapeLock existing = locksByNodeKey.putIfAbsent(nodeKey, lock); if (existing != null) { if (!existing.isExpired()) { throwAlreadyLocked(session, existing); } // there's an existing lock which has expired, so we have to unlock first unlock(session, existing.lockedNodeKey); // try adding the new lock existing = locksByNodeKey.putIfAbsent(nodeKey, lock); if (existing != null) { // some other thread has already replaced the old value throwAlreadyLocked(session, existing); } } try { // Store the lock within the system area ... system.storeLock(lock); // Update the persistent node ... SessionCache lockingSession = session.spawnSessionCache(false); MutableCachedNode lockedNode = lockingSession.mutable(nodeKey); PropertyFactory propertyFactory = session.propertyFactory(); lockedNode.setProperty(lockingSession, propertyFactory.create(JcrLexicon.LOCK_OWNER, owner)); lockedNode.setProperty(lockingSession, propertyFactory.create(JcrLexicon.LOCK_IS_DEEP, isDeep)); lockedNode.lock(isSessionScoped); // Now save both sessions. This will fail with a LockFailureException if the locking failed ... // save the system session first so that the system change is reflected first in the ws caches systemSession.save(lockingSession, null); } catch (LockFailureException | DocumentAlreadyExistsException e) { // Someone must have snuck in and locked the node, and we just didn't receive notification of it yet ... // note that the latter exception is a rare case which can occur only in a cluster under precise timing, when 2 // or more writers attempt to create the same lock the first time String location = nodeKey.toString(); try { location = session.node(nodeKey, null).getPath(); } catch (Throwable t) { // couldn't come up with the path, so just use the key } locksByNodeKey.remove(nodeKey); throw new LockException(JcrI18n.alreadyLocked.text(location)); } catch (RuntimeException e) { locksByNodeKey.remove(nodeKey); throw new RepositoryException(e); } return lock; } private ModeShapeLock throwAlreadyLocked(JcrSession session, ModeShapeLock existing) throws LockException { NodeCache cache = session.cache(); CachedNode locked = cache.getNode(existing.getLockedNodeKey()); String lockedPath = session.stringFactory().create(locked.getPath(cache)); throw new LockException(JcrI18n.alreadyLocked.text(lockedPath)); } /** * 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 * @return true if the lock "held" status was successfully changed to the desired value, or false otherwise * @throws LockException if there is no such lock with the supplied token */ boolean setHeldBySession( JcrSession session, String lockToken, boolean value ) throws LockException { assert lockToken != null; // Create a system session to remove the locks ... final ExecutionContext context = session.context(); SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); // Mark the session as held/unheld ... if (!system.changeLockHeldBySession(lockToken, value)) { return false; } // Now save the session ... system.save(); return true; } String unlock( JcrSession session, NodeKey lockedNodeKey ) throws LockException { ModeShapeLock existing = locksByNodeKey.remove(lockedNodeKey); if (existing == null) { NodeCache cache = session.cache(); String location = session.stringFactory().create(cache.getNode(lockedNodeKey).getPath(cache)); throw new LockException(JcrI18n.notLocked.text(location)); } unlock(session, Collections.singleton(existing)); return existing.getLockToken(); } private void unlock( JcrSession session, Iterable<ModeShapeLock> locks ) { if (locks == null) return; // Create a system session to remove the locks ... final ExecutionContext context = session.context(); SessionCache systemSession = repository.createSystemSession(context, false); SystemContent system = new SystemContent(systemSession); // And create a separate session cache to change the locked nodes ... SessionCache lockingSession = session.spawnSessionCache(false); // Remove the locks ... for (ModeShapeLock lock : locks) { system.removeLock(lock); NodeKey lockedNodeKey = lock.getLockedNodeKey(); if (session.cache().getNode(lockedNodeKey) == null) { // the node on which the lock was placed, has been removed continue; } MutableCachedNode lockedNode = lockingSession.mutable(lockedNodeKey); lockedNode.removeProperty(lockingSession, JcrLexicon.LOCK_IS_DEEP); lockedNode.removeProperty(lockingSession, JcrLexicon.LOCK_OWNER); lockedNode.unlock(); } // Now save the two sessions ... // save the system session first so that the system change is reflected first in the ws caches systemSession.save(lockingSession, null); } /** * Unlocks all locks corresponding to the tokens held by the supplied session. * * @param session the session on behalf of which the lock operation is being performed * @return a {@link Set} of the lock tokens that have been cleaned (removed from the system area and the corresponding nodes * unlocked) * @throws RepositoryException if the session is not live */ Set<String> cleanLocks( JcrSession session ) throws RepositoryException { Set<String> lockTokens = session.lockManager().lockTokens(); List<ModeShapeLock> locks = null; for (ModeShapeLock lock : locksByNodeKey.values()) { if (lock.isSessionScoped() && lockTokens.contains(lock.getLockToken())) { if (locks == null) locks = new LinkedList<ModeShapeLock>(); locks.add(lock); } } Set<String> cleanedTokens = null; if (locks != null) { cleanedTokens = new HashSet<>(locks.size()); // clear the locks which have been unlocked unlock(session, locks); for (ModeShapeLock lock : locks) { locksByNodeKey.remove(lock.getLockedNodeKey()); cleanedTokens.add(lock.getLockToken()); } } return cleanedTokens != null ? cleanedTokens : Collections.<String>emptySet(); } @Override public void notify( ChangeSet changeSet ) { if (!systemWorkspaceName.equals(changeSet.getWorkspaceName())) { // The change does not affect the 'system' workspace, so skip it ... return; } if (processId.equals(changeSet.getProcessKey())) { // We generated these changes, so skip them ... return; } try { // Now process the changes ... Set<NodeKey> locksToDelete = null; for (Change change : changeSet) { if (change instanceof NodeAdded) { NodeAdded added = (NodeAdded)change; Path addedPath = added.getPath(); if (locksPath.isAncestorOf(addedPath)) { // Get the name of the node type ... Map<Name, Property> props = added.getProperties(); NodeKey lockKey = added.getKey(); ModeShapeLock lock = new ModeShapeLock(lockKey, props); locksByNodeKey.put(lock.getLockedNodeKey(), lock); } } else if (change instanceof NodeRemoved) { NodeRemoved removed = (NodeRemoved)change; Path removedPath = removed.getPath(); if (locksPath.isAncestorOf(removedPath)) { // This was a lock that was removed ... if (locksToDelete == null) locksToDelete = new HashSet<NodeKey>(); // The key of the locked node is embedded in the lock key ... NodeKey lockedNodeKey = lockedNodeKeyFromLockKey(removed.getKey()); locksByNodeKey.remove(lockedNodeKey); } } // Lock nodes are never moved, and properties added or removed, and the only properties changed are those // related to lock expiration, which we don't care about. } } catch (Throwable e) { logger.error(e, JcrI18n.errorCleaningUpLocks, repository.name()); } } protected final String firstString( Property property ) { if (property == null) return null; return repository.context().getValueFactories().getStringFactory().create(property.getFirstValue()); } protected final boolean firstBoolean( Property property ) { if (property == null) return false; return repository.context().getValueFactories().getBooleanFactory().create(property.getFirstValue()); } protected final DateTime firstDate( Property property ) { if (property == null) return null; return repository.context().getValueFactories().getDateFactory().create(property.getFirstValue()); } final ModeShapeLock findLockByToken( String token ) { assert token != null; for (ModeShapeLock lock : locksByNodeKey.values()) { if (token.equals(lock.getLockToken())) { return lock; } } return null; } /** * Internal representation of a locked node. */ @Immutable public class ModeShapeLock { private final NodeKey lockedNodeKey; private final NodeKey lockKey; private final String lockOwner; private final String workspaceName; private final String lockToken; private final boolean deep; private final boolean sessionScoped; private final String lockingSessionId; private final DateTime expiryTime; protected ModeShapeLock( CachedNode lockNode, NodeCache cache ) { this.lockKey = lockNode.getKey(); this.lockedNodeKey = lockedNodeKeyFromLockKey(lockKey); this.workspaceName = firstString(lockNode.getProperty(ModeShapeLexicon.WORKSPACE, cache)); this.lockOwner = firstString(lockNode.getProperty(JcrLexicon.LOCK_OWNER, cache)); this.deep = firstBoolean(lockNode.getProperty(JcrLexicon.LOCK_IS_DEEP, cache)); this.sessionScoped = firstBoolean(lockNode.getProperty(ModeShapeLexicon.IS_SESSION_SCOPED, cache)); this.lockToken = firstString(lockNode.getProperty(ModeShapeLexicon.LOCK_TOKEN, cache)); this.lockingSessionId = firstString(lockNode.getProperty(ModeShapeLexicon.LOCKING_SESSION, cache)); this.expiryTime = firstDate(lockNode.getProperty(ModeShapeLexicon.EXPIRATION_DATE, cache)); } protected ModeShapeLock( NodeKey lockKey, Map<Name, Property> properties ) { this.lockKey = lockKey; this.lockedNodeKey = lockedNodeKeyFromLockKey(lockKey); this.workspaceName = firstString(properties.get(ModeShapeLexicon.WORKSPACE)); this.lockOwner = firstString(properties.get(JcrLexicon.LOCK_OWNER)); this.deep = firstBoolean(properties.get(JcrLexicon.LOCK_IS_DEEP)); this.sessionScoped = firstBoolean(properties.get(ModeShapeLexicon.IS_SESSION_SCOPED)); this.lockToken = firstString(properties.get(ModeShapeLexicon.LOCK_TOKEN)); this.lockingSessionId = firstString(properties.get(ModeShapeLexicon.LOCKING_SESSION)); this.expiryTime = firstDate(properties.get(ModeShapeLexicon.EXPIRATION_DATE)); } protected ModeShapeLock( NodeKey lockedNodeKey, NodeKey lockKey, String workspace, String lockOwner, String lockToken, boolean deep, boolean sessionScoped, String lockingSessionId, DateTime expiryTime ) { this.lockedNodeKey = lockedNodeKey; this.lockKey = lockKey; this.lockOwner = lockOwner; this.workspaceName = workspace; this.deep = deep; this.sessionScoped = sessionScoped; this.lockToken = lockToken; this.lockingSessionId = lockingSessionId; this.expiryTime = expiryTime; } protected ModeShapeLock withExpiryTime(DateTime expiryTime) { return new ModeShapeLock(this.lockedNodeKey, this.lockKey, this.workspaceName, this.lockOwner, this.lockToken, this.deep, this.sessionScoped, this.lockingSessionId, expiryTime); } public boolean isLive() { return isLocked(lockedNodeKey) && !isExpired(); } protected boolean isExpired() { return expiryTime.getMilliseconds() < System.currentTimeMillis(); } protected long secondsRemaining() { if (!isLive()) { return Long.MIN_VALUE; } if (isSessionScoped()) { // session scoped locks are tied to the lifetime of the session, so always report them as "never expired" or "unknown" return Long.MAX_VALUE; } long millisRemaining = expiryTime.getMilliseconds() - System.currentTimeMillis(); long secondsRemaining = TimeUnit.SECONDS.convert(millisRemaining, TimeUnit.MILLISECONDS); if (secondsRemaining > 0) { return secondsRemaining; } else if (millisRemaining > 0) { // JCR expects seconds but internally we're using millis and this method has to correlate (as per TCK) // with #isLive. As such, when there's less than a second to go, but still not expired, we'll return MAX_VALUE // which is the equivalent of UNKNOWN as per the docs return Long.MAX_VALUE; } // the lock has expired return Long.MIN_VALUE; } public NodeKey getLockKey() { return lockKey; } public NodeKey getLockedNodeKey() { return lockedNodeKey; } public String getWorkspaceName() { return workspaceName; } public boolean isDeep() { return deep; } public String getLockOwner() { return lockOwner; } public boolean isSessionScoped() { return sessionScoped; } public String getLockToken() { return lockToken; } public String getLockingSessionId() { return lockingSessionId; } public DateTime getExpiryTime() { return expiryTime; } @SuppressWarnings( "synthetic-access" ) public Lock lockFor( final JcrSession session ) throws RepositoryException { final AbstractJcrNode node = session.node(lockedNodeKey, null); final JcrLockManager lockManager = session.workspace().getLockManager(); return new Lock() { @Override public String getLockOwner() { return lockOwner; } @Override public String getLockToken() { if (sessionScoped) return null; String token = ModeShapeLock.this.getLockToken(); return lockManager.hasLockToken(token) ? token : null; } protected final String lockToken() { return ModeShapeLock.this.getLockToken(); } @Override public Node getNode() { return node; } @Override public boolean isDeep() { return deep; } @Override public boolean isLive() { return ModeShapeLock.this.isLive(); } @Override public boolean isSessionScoped() { return sessionScoped; } @Override public void refresh() throws LockException { String token = lockToken(); if (!lockManager.hasLockToken(token)) { String location = null; try { location = node.getPath(); } catch (RepositoryException e) { location = lockedNodeKey.toString(); } throw new LockException(JcrI18n.notLocked.text(location)); } } @Override public long getSecondsRemaining() { return ModeShapeLock.this.secondsRemaining(); } @Override public boolean isLockOwningSession() { String token = lockToken(); return lockManager.hasLockToken(token); } @Override public NodeKey lockKey() { return lockKey; } }; } @Override public String toString() { return "Lock " + lockKey + " for " + lockedNodeKey + " in '" + workspaceName + "' (" + (deep ? "deep," : "shallow;") + (sessionScoped ? "session;" : "global;") + "owner='" + lockOwner + "';token='" + lockToken + "';"; } } protected interface Lock extends javax.jcr.lock.Lock { NodeKey lockKey(); } }