View Javadoc

1   package org.modeshape.jcr;
2   
3   import java.util.Collection;
4   import java.util.UUID;
5   import java.util.concurrent.ConcurrentHashMap;
6   import java.util.concurrent.ConcurrentMap;
7   import javax.jcr.Item;
8   import javax.jcr.Node;
9   import javax.jcr.PropertyType;
10  import javax.jcr.RepositoryException;
11  import javax.jcr.Session;
12  import javax.jcr.lock.Lock;
13  import javax.jcr.lock.LockException;
14  import net.jcip.annotations.ThreadSafe;
15  import org.modeshape.graph.ExecutionContext;
16  import org.modeshape.graph.Graph;
17  import org.modeshape.graph.Location;
18  import org.modeshape.graph.connector.LockFailedException;
19  import org.modeshape.graph.property.DateTime;
20  import org.modeshape.graph.property.DateTimeFactory;
21  import org.modeshape.graph.property.Path;
22  import org.modeshape.graph.property.PathFactory;
23  import org.modeshape.graph.property.PathNotFoundException;
24  import org.modeshape.graph.property.Property;
25  import org.modeshape.graph.property.PropertyFactory;
26  import org.modeshape.graph.property.ValueFactory;
27  import org.modeshape.jcr.SessionCache.NodeEditor;
28  
29  /**
30   * Manages the locks for a particular workspace in a repository. Locks are stored in a {@code Map<UUID, DnaLock>} while they exist
31   * and are discarded after they are discarded (through the {@link Node#unlock()} method).
32   */
33  @ThreadSafe
34  class WorkspaceLockManager {
35  
36      private final ExecutionContext context;
37      private final Path locksPath;
38      private final JcrRepository repository;
39      private final String workspaceName;
40      private final ConcurrentMap<UUID, ModeShapeLock> workspaceLocksByNodeUuid;
41  
42      WorkspaceLockManager( ExecutionContext context,
43                            JcrRepository repository,
44                            String workspaceName,
45                            Path locksPath ) {
46          this.context = context;
47          this.repository = repository;
48          this.workspaceName = workspaceName;
49          this.locksPath = locksPath;
50  
51          this.workspaceLocksByNodeUuid = new ConcurrentHashMap<UUID, ModeShapeLock>();
52  
53          Property locksPrimaryType = context.getPropertyFactory().create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.LOCKS);
54          repository.createSystemGraph(context).create(locksPath, locksPrimaryType).ifAbsent().and();
55      }
56  
57      /**
58       * Creates a lock on the node with the given {@link Location}. This method creates a new lock, registers it in the list of
59       * locks, immediately modifies the {@code jcr:lockOwner} and {@code jcr:lockIsDeep} properties on the node in the underlying
60       * repository, and adds the lock to the system view.
61       * <p>
62       * The location given in {@code nodeLocation} must have a UUID.
63       * </p>
64       * 
65       * @param session the session in which the node is being locked and that loaded the node
66       * @param nodeLocation the location for the node; may not be null and must have a UUID
67       * @param isDeep whether the node's descendants in the content graph should also be locked
68       * @param isSessionScoped whether the lock should outlive the session in which it was created
69       * @return an object representing the newly created lock
70       * @throws RepositoryException if an error occurs updating the graph state
71       */
72      ModeShapeLock lock( JcrSession session,
73                    Location nodeLocation,
74                    boolean isDeep,
75                    boolean isSessionScoped ) throws RepositoryException {
76          assert nodeLocation != null;
77  
78          UUID lockUuid = UUID.randomUUID();
79          UUID nodeUuid = uuidFor(session, nodeLocation);
80  
81          if (nodeUuid == null) {
82              throw new RepositoryException(JcrI18n.uuidRequiredForLock.text(nodeLocation));
83          }
84  
85          ExecutionContext sessionContext = session.getExecutionContext();
86          String lockOwner = session.getUserID();
87          ModeShapeLock lock = createLock(lockOwner, lockUuid, nodeUuid, isDeep, isSessionScoped);
88  
89          Graph.Batch batch = repository.createSystemGraph(sessionContext).batch();
90  
91          PropertyFactory propFactory = sessionContext.getPropertyFactory();
92          PathFactory pathFactory = sessionContext.getValueFactories().getPathFactory();
93          Property lockOwnerProp = propFactory.create(JcrLexicon.LOCK_OWNER, lockOwner);
94          Property lockIsDeepProp = propFactory.create(JcrLexicon.LOCK_IS_DEEP, isDeep);
95  
96          DateTimeFactory dateFactory = sessionContext.getValueFactories().getDateFactory();
97          DateTime expirationDate = dateFactory.create();
98          expirationDate = expirationDate.plusMillis(JcrEngine.LOCK_EXTENSION_INTERVAL_IN_MILLIS);
99  
100         batch.create(pathFactory.create(locksPath, pathFactory.createSegment(lockUuid.toString())),
101                      propFactory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.LOCK),
102                      propFactory.create(ModeShapeLexicon.WORKSPACE, workspaceName),
103                      propFactory.create(ModeShapeLexicon.LOCKED_UUID, nodeUuid.toString()),
104                      propFactory.create(ModeShapeLexicon.IS_SESSION_SCOPED, isSessionScoped),
105                      propFactory.create(ModeShapeLexicon.LOCKING_SESSION, session.sessionId()),
106                      propFactory.create(ModeShapeLexicon.EXPIRATION_DATE, expirationDate),
107                      // This gets set after the lock succeeds and the lock token gets added to the session
108                      propFactory.create(ModeShapeLexicon.IS_HELD_BY_SESSION, false),
109                      lockOwnerProp,
110                      lockIsDeepProp).ifAbsent().and();
111         batch.execute();
112 
113         SessionCache cache = session.cache();
114         AbstractJcrNode lockedNode = cache.findJcrNode(Location.create(nodeUuid));
115         NodeEditor editor = cache.getEditorFor(lockedNode.nodeInfo());
116 
117         // Set the properties in the cache...
118         editor.setProperty(JcrLexicon.LOCK_OWNER,
119                            (JcrValue)cache.session().getValueFactory().createValue(lockOwner, PropertyType.STRING),
120                            false);
121         editor.setProperty(JcrLexicon.LOCK_IS_DEEP, (JcrValue)cache.session().getValueFactory().createValue(isDeep), false);
122 
123         lockNodeInRepository(session, nodeUuid, lockOwnerProp, lockIsDeepProp, lock, isDeep);
124         workspaceLocksByNodeUuid.put(nodeUuid, lock);
125 
126         return lock;
127     }
128 
129     ModeShapeLock createLock( org.modeshape.graph.Node lockNode ) {
130         return new ModeShapeLock(lockNode);
131     }
132 
133     /* Factory method added to facilitate mocked testing */
134     ModeShapeLock createLock( String lockOwner,
135                         UUID lockUuid,
136                         UUID nodeUuid,
137                         boolean isDeep,
138                         boolean isSessionScoped ) {
139         return new ModeShapeLock(lockOwner, lockUuid, nodeUuid, isDeep, isSessionScoped);
140     }
141 
142     /**
143      * Marks the node as locked in the underlying repository with an immediate write (that is, the write is not part of the JCR
144      * session scope and cannot be "rolled back" with a refresh at the {@link Session#refresh(boolean) session} or
145      * {@link Item#refresh(boolean) item} level.
146      * <p>
147      * This method will also attempt to {@link Graph#lock(Location) lock the node in the underlying repository}. If the underlying
148      * repository supports locks and {@link LockFailedException the lock attempt fails}, this method will cancel the lock attempt
149      * by calling {@link #unlock(ExecutionContext, ModeShapeLock)} and will throw a {@code RepositoryException}.
150      * </p>
151      * <p>
152      * This method does not modify the system graph. In other words, it will not create the record for the lock in the {@code
153      * /jcr:system/dna:locks} subgraph.
154      * </p>
155      * 
156      * @param session the session in which the node is being locked and that loaded the node
157      * @param nodeUuid the UUID of the node to lock
158      * @param lockOwnerProp an existing property with name {@link JcrLexicon#LOCK_OWNER} and the value being the name of the lock
159      *        owner
160      * @param lockIsDeepProp an existing property with name {@link JcrLexicon#LOCK_IS_DEEP} and the value being {@code true} if
161      *        the lock should include all descendants of the locked node or {@code false} if the lock should only include the
162      *        specified node and not its descendants
163      * @param lock the internal lock representation
164      * @param isDeep {@code true} if the lock should include all descendants of the locked node or {@code false} if the lock
165      *        should only include the specified node and not its descendants. This value is redundant with the lockIsDeep
166      *        parameter, but is included separately as a minor performance optimization
167      * @throws RepositoryException if the repository in which the node represented by {@code nodeUuid} supports locking but
168      *         signals that the lock for the node cannot be acquired
169      */
170     void lockNodeInRepository( JcrSession session,
171                                UUID nodeUuid,
172                                Property lockOwnerProp,
173                                Property lockIsDeepProp,
174                                ModeShapeLock lock,
175                                boolean isDeep ) throws RepositoryException {
176         // Write them directly to the underlying graph
177         Graph.Batch workspaceBatch = repository.createWorkspaceGraph(workspaceName, session.getExecutionContext()).batch();
178         workspaceBatch.set(lockOwnerProp, lockIsDeepProp).on(nodeUuid);
179         if (isDeep) {
180             workspaceBatch.lock(nodeUuid).andItsDescendants().withDefaultTimeout();
181         } else {
182             workspaceBatch.lock(nodeUuid).only().withDefaultTimeout();
183         }
184         try {
185             workspaceBatch.execute();
186         } catch (LockFailedException lfe) {
187             // Attempt to lock node at the repo level failed - cancel lock
188             unlock(session.getExecutionContext(), lock);
189             throw new RepositoryException(lfe);
190         }
191 
192     }
193 
194     /**
195      * Removes the provided lock, effectively unlocking the node to which the lock is associated.
196      * 
197      * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
198      * @param lock the lock to be removed
199      */
200     void unlock( ExecutionContext sessionExecutionContext,
201                  ModeShapeLock lock ) {
202         try {
203             PathFactory pathFactory = sessionExecutionContext.getValueFactories().getPathFactory();
204 
205             // Remove the lock node under the /jcr:system branch ...
206             Graph.Batch batch = repository.createSystemGraph(sessionExecutionContext).batch();
207             batch.delete(pathFactory.create(locksPath, pathFactory.createSegment(lock.getUuid().toString())));
208             batch.execute();
209 
210             // Unlock the node in the repository graph ...
211             unlockNodeInRepository(sessionExecutionContext, lock);
212 
213             workspaceLocksByNodeUuid.remove(lock.nodeUuid);
214         } catch (PathNotFoundException pnfe) {
215             /*
216              * 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
217              * 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.
218              */
219             if (!lock.nodeUuid.equals(pnfe.getLocation().getUuid())) {
220                 // If the lock node under dna:locks is not found, this is an internal error
221                 throw new IllegalStateException(pnfe);
222             }
223             workspaceLocksByNodeUuid.remove(lock.nodeUuid);
224         }
225     }
226 
227     /**
228      * Removes the workspace record of the lock in the underlying repository. This method clears the {@code jcr:lockOwner} and
229      * {@code jcr:lockIsDeep} properties on the node and sends an {@link Graph#unlock(Location) unlock request} to the underlying
230      * repository to clear any locks that it is holding on the node.
231      * <p>
232      * This method does not modify the system graph. In other words, it will not remove the record for the lock in the {@code
233      * /jcr:system/dna:locks} subgraph.
234      * </p>
235      * 
236      * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
237      * @param lock
238      */
239     void unlockNodeInRepository( ExecutionContext sessionExecutionContext,
240                                  ModeShapeLock lock ) {
241         Graph.Batch workspaceBatch = repository.createWorkspaceGraph(this.workspaceName, sessionExecutionContext).batch();
242 
243         workspaceBatch.remove(JcrLexicon.LOCK_OWNER, JcrLexicon.LOCK_IS_DEEP).on(lock.nodeUuid);
244         workspaceBatch.unlock(lock.nodeUuid);
245 
246         workspaceBatch.execute();
247     }
248 
249     /**
250      * Checks whether the given lock token is currently held by any session by querying the lock record in the underlying
251      * repository.
252      * 
253      * @param session the session on behalf of which the lock query is being performed
254      * @param lockToken the lock token to check; may not be null
255      * @return true if a session currently holds the lock token, false otherwise
256      */
257     boolean isHeldBySession( JcrSession session,
258                              String lockToken ) {
259         assert lockToken != null;
260 
261         ExecutionContext context = session.getExecutionContext();
262         ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();
263         PathFactory pathFactory = context.getValueFactories().getPathFactory();
264 
265         org.modeshape.graph.Node lockNode = repository.createSystemGraph(context)
266                                                       .getNodeAt(pathFactory.create(locksPath,
267                                                                                     pathFactory.createSegment(lockToken)));
268 
269         return booleanFactory.create(lockNode.getProperty(ModeShapeLexicon.IS_HELD_BY_SESSION).getFirstValue());
270 
271     }
272 
273     /**
274      * Updates the underlying repository directly (i.e., outside the scope of the {@link Session}) to mark the token for the given
275      * lock as being held (or not held) by some {@link Session}. Note that this method does not identify <i>which</i> (if any)
276      * session holds the token for the lock, just that <i>some</i> session holds the token for the lock.
277      * 
278      * @param session the session on behalf of which the lock operation is being performed
279      * @param lockToken the lock token for which the "held" status should be modified; may not be null
280      * @param value the new value
281      */
282     void setHeldBySession( JcrSession session,
283                            String lockToken,
284                            boolean value ) {
285         assert lockToken != null;
286 
287         ExecutionContext context = session.getExecutionContext();
288         PropertyFactory propFactory = context.getPropertyFactory();
289         PathFactory pathFactory = context.getValueFactories().getPathFactory();
290 
291         repository.createSystemGraph(context)
292                   .set(propFactory.create(ModeShapeLexicon.IS_HELD_BY_SESSION, value))
293                   .on(pathFactory.create(locksPath, pathFactory.createSegment(lockToken)));
294     }
295 
296     /**
297      * Returns the lock that corresponds to the given lock token
298      * 
299      * @param lockToken the lock token
300      * @return the corresponding lock, possibly null
301      * @see Session#addLockToken(String)
302      * @see Session#removeLockToken(String)
303      */
304     ModeShapeLock lockFor( String lockToken ) {
305         for (ModeShapeLock lock : workspaceLocksByNodeUuid.values()) {
306             if (lockToken.equals(lock.getLockToken())) {
307                 return lock;
308             }
309         }
310 
311         return null;
312     }
313 
314     /**
315      * Returns the lock that corresponds to the given UUID
316      * 
317      * @param session the session on behalf of which the lock operation is being performed
318      * @param nodeLocation the node UUID
319      * @return the corresponding lock, possibly null if there is no such lock
320      */
321     ModeShapeLock lockFor( JcrSession session,
322                      Location nodeLocation ) {
323         UUID nodeUuid = uuidFor(session, nodeLocation);
324         if (nodeUuid == null) return null;
325         return workspaceLocksByNodeUuid.get(nodeUuid);
326     }
327 
328     /**
329      * Returns the UUID that identifies the given location or {@code null} if the location does not have a UUID. The method
330      * returns the {@link Location#getUuid() default UUID} if it exists. If it does not, the method returns the value of the
331      * {@link JcrLexicon#UUID} property as a UUID. If the location does not contain that property, the method returns null.
332      * 
333      * @param session the session on behalf of which the lock operation is being performed
334      * @param location the location for which the UUID should be returned
335      * @return the UUID that identifies the given location or {@code null} if the location does not have a UUID.
336      */
337     UUID uuidFor( JcrSession session,
338                   Location location ) {
339         assert location != null;
340 
341         if (location.getUuid() != null) return location.getUuid();
342 
343         org.modeshape.graph.property.Property uuidProp = location.getIdProperty(JcrLexicon.UUID);
344         if (uuidProp == null) return null;
345 
346         ExecutionContext context = session.getExecutionContext();
347         return context.getValueFactories().getUuidFactory().create(uuidProp.getFirstValue());
348     }
349 
350     /**
351      * Unlocks all locks corresponding to the tokens in the {@code lockTokens} collection that are session scoped.
352      * 
353      * @param session the session on behalf of which the lock operation is being performed
354      */
355     void cleanLocks( JcrSession session ) {
356         ExecutionContext context = session.getExecutionContext();
357         Collection<String> lockTokens = session.lockTokens();
358         for (String lockToken : lockTokens) {
359             ModeShapeLock lock = lockFor(lockToken);
360             if (lock != null && lock.isSessionScoped()) {
361                 unlock(context, lock);
362             }
363         }
364     }
365 
366     /**
367      * Internal representation of a locked node. This class should only be created through calls to
368      * {@link WorkspaceLockManager#lock(JcrSession, Location, boolean, boolean)}.
369      */
370     @ThreadSafe
371     public class ModeShapeLock {
372         final UUID nodeUuid;
373         private final UUID lockUuid;
374         private final String lockOwner;
375         private final boolean deep;
376         private final boolean sessionScoped;
377 
378         @SuppressWarnings( "synthetic-access" )
379         ModeShapeLock( org.modeshape.graph.Node lockNode ) {
380             ValueFactory<String> stringFactory = context.getValueFactories().getStringFactory();
381             ValueFactory<UUID> uuidFactory = context.getValueFactories().getUuidFactory();
382             ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();
383 
384             assert lockNode.getLocation().getPath() != null;
385 
386             String lockUuidAsString = lockNode.getLocation().getPath().getLastSegment().getName().getLocalName();
387             Property lockOwnerProperty = lockNode.getProperty(JcrLexicon.LOCK_OWNER);
388             Property nodeUuidProperty = lockNode.getProperty(ModeShapeLexicon.LOCKED_UUID);
389             Property lockIsDeepProperty = lockNode.getProperty(JcrLexicon.LOCK_IS_DEEP);
390             Property isSessionScopedProperty = lockNode.getProperty(ModeShapeLexicon.IS_SESSION_SCOPED);
391 
392             assert lockUuidAsString != null;
393             assert lockOwnerProperty != null;
394             assert nodeUuidProperty != null;
395             assert lockIsDeepProperty != null;
396             assert isSessionScopedProperty != null;
397 
398             this.lockOwner = stringFactory.create(lockOwnerProperty.getFirstValue());
399             this.lockUuid = UUID.fromString(lockUuidAsString);
400             this.nodeUuid = uuidFactory.create(nodeUuidProperty.getFirstValue());
401             this.deep = booleanFactory.create(lockIsDeepProperty.getFirstValue());
402             this.sessionScoped = booleanFactory.create(isSessionScopedProperty.getFirstValue());
403         }
404 
405         ModeShapeLock( String lockOwner,
406                  UUID lockUuid,
407                  UUID nodeUuid,
408                  boolean deep,
409                  boolean sessionScoped ) {
410             super();
411             this.lockOwner = lockOwner;
412             this.lockUuid = lockUuid;
413             this.nodeUuid = nodeUuid;
414             this.deep = deep;
415             this.sessionScoped = sessionScoped;
416         }
417 
418         @SuppressWarnings( "synthetic-access" )
419         public boolean isLive() {
420             return workspaceLocksByNodeUuid.containsKey(nodeUuid);
421         }
422 
423         public UUID getUuid() {
424             return lockUuid;
425         }
426 
427         public boolean isDeep() {
428             return deep;
429         }
430 
431         public String getLockOwner() {
432             return lockOwner;
433         }
434 
435         public boolean isSessionScoped() {
436             return sessionScoped;
437         }
438 
439         public String getLockToken() {
440             return lockUuid.toString();
441         }
442 
443         @SuppressWarnings( "synthetic-access" )
444         public Lock lockFor( SessionCache cache ) throws RepositoryException {
445             final AbstractJcrNode node = cache.findJcrNode(Location.create(nodeUuid));
446             final JcrSession session = cache.session();
447             return new Lock() {
448                 public String getLockOwner() {
449                     return lockOwner;
450                 }
451 
452                 public String getLockToken() {
453                     String uuidString = lockUuid.toString();
454                     return session.lockTokens().contains(uuidString) ? uuidString : null;
455                 }
456 
457                 public Node getNode() {
458                     return node;
459                 }
460 
461                 public boolean isDeep() {
462                     return deep;
463                 }
464 
465                 public boolean isLive() {
466                     return workspaceLocksByNodeUuid.containsKey(nodeUuid);
467                 }
468 
469                 public boolean isSessionScoped() {
470                     return sessionScoped;
471                 }
472 
473                 public void refresh() throws LockException {
474                     if (getLockToken() == null) {
475                         throw new LockException(JcrI18n.notLocked.text(node.location));
476                     }
477                 }
478             };
479         }
480 
481     }
482 }