View Javadoc

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