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.graph.request.processor;
25  
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collections;
29  import java.util.Iterator;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Queue;
34  import net.jcip.annotations.Immutable;
35  import net.jcip.annotations.NotThreadSafe;
36  import org.modeshape.common.util.CheckArg;
37  import org.modeshape.graph.ExecutionContext;
38  import org.modeshape.graph.GraphI18n;
39  import org.modeshape.graph.Location;
40  import org.modeshape.graph.cache.CachePolicy;
41  import org.modeshape.graph.connector.LockFailedException;
42  import org.modeshape.graph.observe.Changes;
43  import org.modeshape.graph.observe.Observer;
44  import org.modeshape.graph.property.DateTime;
45  import org.modeshape.graph.property.Name;
46  import org.modeshape.graph.property.Path;
47  import org.modeshape.graph.property.Property;
48  import org.modeshape.graph.property.ReferentialIntegrityException;
49  import org.modeshape.graph.request.AccessQueryRequest;
50  import org.modeshape.graph.request.CacheableRequest;
51  import org.modeshape.graph.request.ChangeRequest;
52  import org.modeshape.graph.request.CloneBranchRequest;
53  import org.modeshape.graph.request.CloneWorkspaceRequest;
54  import org.modeshape.graph.request.CollectGarbageRequest;
55  import org.modeshape.graph.request.CompositeRequest;
56  import org.modeshape.graph.request.CopyBranchRequest;
57  import org.modeshape.graph.request.CreateNodeRequest;
58  import org.modeshape.graph.request.CreateWorkspaceRequest;
59  import org.modeshape.graph.request.DeleteBranchRequest;
60  import org.modeshape.graph.request.DeleteChildrenRequest;
61  import org.modeshape.graph.request.DestroyWorkspaceRequest;
62  import org.modeshape.graph.request.FullTextSearchRequest;
63  import org.modeshape.graph.request.GetWorkspacesRequest;
64  import org.modeshape.graph.request.InvalidRequestException;
65  import org.modeshape.graph.request.LockBranchRequest;
66  import org.modeshape.graph.request.MoveBranchRequest;
67  import org.modeshape.graph.request.ReadAllChildrenRequest;
68  import org.modeshape.graph.request.ReadAllPropertiesRequest;
69  import org.modeshape.graph.request.ReadBlockOfChildrenRequest;
70  import org.modeshape.graph.request.ReadBranchRequest;
71  import org.modeshape.graph.request.ReadNextBlockOfChildrenRequest;
72  import org.modeshape.graph.request.ReadNodeRequest;
73  import org.modeshape.graph.request.ReadPropertyRequest;
74  import org.modeshape.graph.request.RemovePropertyRequest;
75  import org.modeshape.graph.request.RenameNodeRequest;
76  import org.modeshape.graph.request.Request;
77  import org.modeshape.graph.request.SetPropertyRequest;
78  import org.modeshape.graph.request.UnlockBranchRequest;
79  import org.modeshape.graph.request.UnsupportedRequestException;
80  import org.modeshape.graph.request.UpdatePropertiesRequest;
81  import org.modeshape.graph.request.UpdateValuesRequest;
82  import org.modeshape.graph.request.VerifyNodeExistsRequest;
83  import org.modeshape.graph.request.VerifyWorkspaceRequest;
84  
85  /**
86   * A component that is used to process and execute {@link Request}s. This class is intended to be subclassed and methods
87   * overwritten to define the behavior for executing the different kinds of requests. Abstract methods must be overridden, but
88   * non-abstract methods all have meaningful default implementations.
89   */
90  @NotThreadSafe
91  public abstract class RequestProcessor {
92  
93      private final ExecutionContext context;
94      private final String sourceName;
95      private final DateTime nowInUtc;
96      private final CachePolicy defaultCachePolicy;
97      private List<ChangeRequest> changes;
98      private final Observer observer;
99  
100     protected RequestProcessor( String sourceName,
101                                 ExecutionContext context,
102                                 Observer observer ) {
103         this(sourceName, context, observer, null, null);
104     }
105 
106     protected RequestProcessor( String sourceName,
107                                 ExecutionContext context,
108                                 Observer observer,
109                                 DateTime now ) {
110         this(sourceName, context, observer, now, null);
111     }
112 
113     protected RequestProcessor( String sourceName,
114                                 ExecutionContext context,
115                                 Observer observer,
116                                 DateTime now,
117                                 CachePolicy defaultCachePolicy ) {
118         CheckArg.isNotEmpty(sourceName, "sourceName");
119         CheckArg.isNotNull(context, "context");
120         this.context = context;
121         this.sourceName = sourceName;
122         this.nowInUtc = now != null ? now : context.getValueFactories().getDateFactory().createUtc();
123         this.defaultCachePolicy = defaultCachePolicy;
124         this.changes = observer != null ? new LinkedList<ChangeRequest>() : null;
125         this.observer = observer;
126     }
127 
128     /**
129      * Record the supplied change request for publishing through the event mechanism.
130      * 
131      * @param request the completed change request; may not be null, and may not be cancelled or have an error
132      */
133     protected void recordChange( ChangeRequest request ) {
134         assert request != null;
135         assert !request.isCancelled();
136         assert !request.hasError();
137         if (changes != null) changes.add(request);
138     }
139 
140     /**
141      * Get the name of the source against which this processor is executing.
142      * 
143      * @return the repository source name; never null or empty
144      */
145     public final String getSourceName() {
146         return sourceName;
147     }
148 
149     /**
150      * The execution context that this process is operating within.
151      * 
152      * @return the execution context; never null
153      */
154     public final ExecutionContext getExecutionContext() {
155         return this.context;
156     }
157 
158     /**
159      * Get the 'current time' for this processor, which is usually a constant during its lifetime.
160      * 
161      * @return the current time in UTC; never null
162      */
163     public final DateTime getNowInUtc() {
164         return this.nowInUtc;
165     }
166 
167     /**
168      * @return defaultCachePolicy
169      */
170     protected final CachePolicy getDefaultCachePolicy() {
171         return defaultCachePolicy;
172     }
173 
174     /**
175      * Set the supplied request to have the default cache policy and the {@link #getNowInUtc() current time in UTC}.
176      * 
177      * @param request the cacheable request
178      */
179     protected void setCacheableInfo( CacheableRequest request ) {
180         // Set it only if the request has no cache policy already ...
181         if (request.getCachePolicy() == null && defaultCachePolicy != null) {
182             request.setCachePolicy(defaultCachePolicy);
183         }
184         request.setTimeLoaded(nowInUtc);
185     }
186 
187     /**
188      * Set the supplied request to have the supplied cache policy and the {@link #getNowInUtc() current time in UTC}.
189      * 
190      * @param request the cacheable request
191      * @param cachePolicy the cache policy for the request; may be null if there is to be no cache policy
192      */
193     protected void setCacheableInfo( CacheableRequest request,
194                                      CachePolicy cachePolicy ) {
195         if (cachePolicy == null) cachePolicy = defaultCachePolicy;
196         if (cachePolicy != null) {
197             if (request.getCachePolicy() != null) {
198                 // Set the supplied only if less than the current ...
199                 if (request.getCachePolicy().getTimeToLive() > cachePolicy.getTimeToLive()) {
200                     request.setCachePolicy(cachePolicy);
201                 }
202             } else {
203                 // There is no current policy, so set the supplied policy ...
204                 request.setCachePolicy(cachePolicy);
205             }
206         }
207         request.setTimeLoaded(nowInUtc);
208     }
209 
210     /**
211      * Process a request by determining the type of request and delegating to the appropriate <code>process</code> method for that
212      * type.
213      * <p>
214      * This method does nothing if the request is null.
215      * </p>
216      * 
217      * @param request the general request
218      */
219     public void process( Request request ) {
220         if (request == null) return;
221         try {
222             if (request.isCancelled()) return;
223 
224             switch (request.getType()) {
225                 case ACCESS_QUERY:
226                     process((AccessQueryRequest)request);
227                     break;
228                 case COMPOSITE:
229                     process((CompositeRequest)request);
230                     break;
231                 case CLONE_BRANCH:
232                     process((CloneBranchRequest)request);
233                     break;
234                 case CLONE_WORKSPACE:
235                     process((CloneWorkspaceRequest)request);
236                     break;
237                 case COLLECT_GARBAGE:
238                     process((CollectGarbageRequest)request);
239                     break;
240                 case COPY_BRANCH:
241                     process((CopyBranchRequest)request);
242                     break;
243                 case CREATE_NODE:
244                     process((CreateNodeRequest)request);
245                     break;
246                 case CREATE_WORKSPACE:
247                     process((CreateWorkspaceRequest)request);
248                     break;
249                 case DELETE_BRANCH:
250                     process((DeleteBranchRequest)request);
251                     break;
252                 case DELETE_CHILDREN:
253                     process((DeleteChildrenRequest)request);
254                     break;
255                 case DESTROY_WORKSPACE:
256                     process((DestroyWorkspaceRequest)request);
257                     break;
258                 case FULL_TEXT_SEARCH:
259                     process((FullTextSearchRequest)request);
260                     break;
261                 case GET_WORKSPACES:
262                     process((GetWorkspacesRequest)request);
263                     break;
264                 case LAST:
265                     break;
266                 case LOCK_BRANCH:
267                     process((LockBranchRequest)request);
268                     break;
269                 case MOVE_BRANCH:
270                     process((MoveBranchRequest)request);
271                     break;
272                 case READ_ALL_CHILDREN:
273                     process((ReadAllChildrenRequest)request);
274                     break;
275                 case READ_ALL_PROPERTIES:
276                     process((ReadAllPropertiesRequest)request);
277                     break;
278                 case READ_BLOCK_OF_CHILDREN:
279                     process((ReadBlockOfChildrenRequest)request);
280                     break;
281                 case READ_BRANCH:
282                     process((ReadBranchRequest)request);
283                     break;
284                 case READ_NEXT_BLOCK_OF_CHILDREN:
285                     process((ReadNextBlockOfChildrenRequest)request);
286                     break;
287                 case READ_NODE:
288                     process((ReadNodeRequest)request);
289                     break;
290                 case READ_PROPERTY:
291                     process((ReadPropertyRequest)request);
292                     break;
293                 case REMOVE_PROPERTY:
294                     process((RemovePropertyRequest)request);
295                     break;
296                 case RENAME_NODE:
297                     process((RenameNodeRequest)request);
298                     break;
299                 case SET_PROPERTY:
300                     process((SetPropertyRequest)request);
301                     break;
302                 case UNLOCK_BRANCH:
303                     process((UnlockBranchRequest)request);
304                     break;
305                 case UPDATE_PROPERTIES:
306                     process((UpdatePropertiesRequest)request);
307                     break;
308                 case UPDATE_VALUES:
309                     process((UpdateValuesRequest)request);
310                     break;
311                 case VERIFY_NODE_EXISTS:
312                     process((VerifyNodeExistsRequest)request);
313                     break;
314                 case VERIFY_WORKSPACE:
315                     process((VerifyWorkspaceRequest)request);
316                     break;
317                 default:
318                     processUnknownRequest(request);
319             }
320         } catch (RuntimeException e) {
321             request.setError(e);
322         } finally {
323             completeRequest(request);
324         }
325     }
326 
327     protected void completeRequest( Request request ) {
328         request.freeze();
329     }
330 
331     /**
332      * Process a request that is composed of multiple other (non-composite) requests. If any of the embedded requests
333      * {@link Request#hasError() has an error} after it is processed, each of the embedded requests will be marked with the error
334      * <i>and</i> the submitted composite request will be marked with an error. If one of the embedded requests attempts to
335      * {@link Request#isReadOnly() make a change} and results in an error, then the processing of all remaining embedded requests
336      * is aborted and the composite request will be marked with only this error from the change request.
337      * <p>
338      * This method does nothing if the request is null.
339      * </p>
340      * 
341      * @param request the composite request
342      */
343     public void process( CompositeRequest request ) {
344         if (request == null) return;
345         boolean hasErrors = false;
346         boolean readonly = request.isReadOnly();
347         // Iterate over the requests in this composite, but only iterate once so that
348         Iterator<Request> iter = request.iterator();
349         while (iter.hasNext()) {
350             Request embedded = iter.next();
351             assert embedded != null;
352             if (embedded.isCancelled()) return;
353             try {
354                 process(embedded);
355             } catch (RuntimeException e) {
356                 embedded.setError(e);
357             }
358             if (!hasErrors && embedded.hasError()) {
359                 hasErrors = true;
360                 if (!readonly && !embedded.isReadOnly()) {
361                     // The request is trying to make changes, and this embedded was a change that resulted in an error.
362                     // Therefore, the whole execution needs to stop and we should set this one error message
363                     // on the composite request ...
364                     assert embedded.getError() != null;
365                     request.setError(embedded.getError());
366                     // We need to freeze all the remaining (unprocessed) requests before returning ...
367                     while (iter.hasNext()) {
368                         embedded = iter.next();
369                         // Cancel this request and then freeze ...
370                         embedded.cancel();
371                         embedded.freeze();
372                     }
373                     return;
374                 }
375             }
376         }
377         if (hasErrors) {
378             request.checkForErrors();
379         }
380     }
381 
382     /**
383      * Method that is called by {@link #process(Request)} when the request was found to be of a request type that is not known by
384      * this processor. By default this method sets an {@link UnsupportedRequestException unsupported request error} on the
385      * request.
386      * 
387      * @param request the unknown request
388      */
389     protected void processUnknownRequest( Request request ) {
390         request.setError(new InvalidRequestException(GraphI18n.unsupportedRequestType.text(request.getClass().getName(), request)));
391     }
392 
393     /**
394      * Process a request to verify a named workspace.
395      * <p>
396      * This method does nothing if the request is null.
397      * </p>
398      * 
399      * @param request the request
400      */
401     public abstract void process( VerifyWorkspaceRequest request );
402 
403     /**
404      * Process a request to get the information about the available workspaces.
405      * <p>
406      * This method does nothing if the request is null.
407      * </p>
408      * 
409      * @param request the request
410      */
411     public abstract void process( GetWorkspacesRequest request );
412 
413     /**
414      * Process a request to create a new workspace.
415      * <p>
416      * This method does nothing if the request is null.
417      * </p>
418      * 
419      * @param request the request
420      */
421     public abstract void process( CreateWorkspaceRequest request );
422 
423     /**
424      * Process a request to clone a branch into a new workspace.
425      * <p>
426      * This method does nothing if the request is null.
427      * </p>
428      * 
429      * @param request the request
430      */
431     public abstract void process( CloneBranchRequest request );
432 
433     /**
434      * Process a request to clone an existing workspace as a new workspace.
435      * <p>
436      * This method does nothing if the request is null.
437      * </p>
438      * 
439      * @param request the request
440      */
441     public abstract void process( CloneWorkspaceRequest request );
442 
443     /**
444      * Process a request to permanently destroy a workspace.
445      * <p>
446      * This method does nothing if the request is null.
447      * </p>
448      * 
449      * @param request the request
450      */
451     public abstract void process( DestroyWorkspaceRequest request );
452 
453     /**
454      * Process a request to copy a branch into another location.
455      * <p>
456      * This method does nothing if the request is null.
457      * </p>
458      * 
459      * @param request the copy request
460      */
461     public abstract void process( CopyBranchRequest request );
462 
463     /**
464      * Process a request to create a node at a specified location.
465      * <p>
466      * This method does nothing if the request is null.
467      * </p>
468      * 
469      * @param request the create request
470      */
471     public abstract void process( CreateNodeRequest request );
472 
473     /**
474      * Process a request to delete a branch at a specified location.
475      * <p>
476      * This method does nothing if the request is null.
477      * </p>
478      * 
479      * @param request the delete request
480      * @throws ReferentialIntegrityException if the delete could not be performed because some references to deleted nodes would
481      *         have remained after the delete operation completed
482      */
483     public abstract void process( DeleteBranchRequest request );
484 
485     /**
486      * Process a request to delete all of the child nodes under the supplied existing node.
487      * <p>
488      * This method does nothing if the request is null.
489      * </p>
490      * 
491      * @param request the delete request
492      * @throws ReferentialIntegrityException if the delete could not be performed because some references to deleted nodes would
493      *         have remained after the delete operation completed
494      */
495     public void process( DeleteChildrenRequest request ) {
496         if (request == null) return;
497         if (request.isCancelled()) return;
498         // First get all of the children under the node ...
499         ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at(), request.inWorkspace());
500         process(readChildren);
501         if (readChildren.hasError()) {
502             request.setError(readChildren.getError());
503             return;
504         }
505         if (readChildren.isCancelled()) return;
506 
507         // Issue a DeleteBranchRequest for each child ...
508         for (Location child : readChildren) {
509             if (request.isCancelled()) return;
510             DeleteBranchRequest deleteChild = new DeleteBranchRequest(child, request.inWorkspace());
511             process(deleteChild);
512             request.addDeletedChild(child);
513         }
514 
515         // Set the actual location of the parent node ...
516         request.setActualLocationOfNode(readChildren.getActualLocationOfNode());
517     }
518 
519     /**
520      * Process a request to move a branch at a specified location into a different location.
521      * <p>
522      * This method does nothing if the request is null.
523      * </p>
524      * 
525      * @param request the move request
526      */
527     public abstract void process( MoveBranchRequest request );
528 
529     /**
530      * Process a request to read all of the children of a node.
531      * <p>
532      * This method does nothing if the request is null.
533      * </p>
534      * 
535      * @param request the read request
536      */
537     public abstract void process( ReadAllChildrenRequest request );
538 
539     /**
540      * Process a request to read a block of the children of a node. The block is defined by a
541      * {@link ReadBlockOfChildrenRequest#startingAtIndex() starting index} and a {@link ReadBlockOfChildrenRequest#count() maximum
542      * number of children to include in the block}.
543      * <p>
544      * This method does nothing if the request is null. The default implementation converts the command to a
545      * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
546      * implementation may not be efficient and may need to be overridden.
547      * </p>
548      * 
549      * @param request the read request
550      */
551     public void process( ReadBlockOfChildrenRequest request ) {
552         if (request == null) return;
553         // Convert the request to a ReadAllChildrenRequest and execute it ...
554         ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(request.of(), request.inWorkspace());
555         process(readAll);
556         if (readAll.hasError()) {
557             request.setError(readAll.getError());
558             return;
559         }
560         List<Location> allChildren = readAll.getChildren();
561 
562         // If there aren't enough children for the block's range ...
563         if (allChildren.size() < request.startingAtIndex()) return;
564 
565         // Now, find the children in the block ...
566         int endIndex = Math.min(request.endingBefore(), allChildren.size());
567         for (int i = request.startingAtIndex(); i != endIndex; ++i) {
568             request.addChild(allChildren.get(i));
569         }
570         // Set the actual location ...
571         request.setActualLocationOfNode(readAll.getActualLocationOfNode());
572         setCacheableInfo(request);
573     }
574 
575     /**
576      * Process a request to read the next block of the children of a node, starting after a previously-retrieved child.
577      * <p>
578      * This method does nothing if the request is null. The default implementation converts the command to a
579      * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
580      * implementation may not be efficient and may need to be overridden.
581      * </p>
582      * 
583      * @param request the read request
584      */
585     public void process( ReadNextBlockOfChildrenRequest request ) {
586         if (request == null) return;
587 
588         // Get the parent path ...
589         Location actualSiblingLocation = request.startingAfter();
590         Path path = actualSiblingLocation.getPath();
591         Path parentPath = null;
592         if (path != null) parentPath = path.getParent();
593         if (parentPath == null) {
594             // Need to find the parent path, so get the actual location of the sibling ...
595             VerifyNodeExistsRequest verifySibling = new VerifyNodeExistsRequest(request.startingAfter(), request.inWorkspace());
596             process(verifySibling);
597             actualSiblingLocation = verifySibling.getActualLocationOfNode();
598             parentPath = actualSiblingLocation.getPath().getParent();
599         }
600         assert parentPath != null;
601 
602         // Convert the request to a ReadAllChildrenRequest and execute it ...
603         ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(Location.create(parentPath), request.inWorkspace());
604         process(readAll);
605         if (readAll.hasError()) {
606             request.setError(readAll.getError());
607             return;
608         }
609         List<Location> allChildren = readAll.getChildren();
610 
611         // Iterate through the children, looking for the 'startingAfter' child ...
612         boolean found = false;
613         int count = 0;
614         for (Location child : allChildren) {
615             if (count > request.count()) break;
616             if (!found) {
617                 // Set to true if we find the child we're looking for ...
618                 found = child.isSame(request.startingAfter());
619             } else {
620                 // Add the child to the block ...
621                 ++count;
622                 request.addChild(child);
623             }
624         }
625 
626         // Set the actual location ...
627         request.setActualLocationOfStartingAfterNode(actualSiblingLocation);
628         setCacheableInfo(request);
629     }
630 
631     /**
632      * Process a request to read a branch or subgraph that's below a node at a specified location.
633      * <p>
634      * This method does nothing if the request is null. The default implementation processes the branch by submitting the
635      * equivalent requests to {@link ReadNodeRequest read the nodes} and the {@link ReadAllChildrenRequest children}. It starts by
636      * doing this for the top-level node, then proceeds for each of the children of that node, and so forth.
637      * </p>
638      * 
639      * @param request the request to read the branch
640      */
641     public void process( ReadBranchRequest request ) {
642         if (request == null) return;
643         // Create a queue for locations that need to be read ...
644         Queue<LocationWithDepth> locationsToRead = new LinkedList<LocationWithDepth>();
645         locationsToRead.add(new LocationWithDepth(request.at(), 1));
646         int maxDepthPerRead = Math.min(request.maximumDepth(), absoluteMaximumDepthForBranchReads());
647 
648         // Now read the locations ...
649         boolean first = true;
650         while (locationsToRead.peek() != null) {
651             if (request.isCancelled()) return;
652             LocationWithDepth read = locationsToRead.poll();
653 
654             // Check the depth ...
655             if (read.depth > maxDepthPerRead) break;
656 
657             // Read the properties ...
658             ReadNodeRequest readNode = new ReadNodeRequest(read.location, request.inWorkspace());
659             process(readNode);
660             if (readNode.hasError()) {
661                 request.setError(readNode.getError());
662                 return;
663             }
664 
665             Location actualLocation = readNode.getActualLocationOfNode();
666             if (first) {
667                 // Set the actual location on the original request
668                 request.setActualLocationOfNode(actualLocation);
669             }
670 
671             // Record in the request the children and properties that were read on this node ...
672             request.setChildren(actualLocation, readNode.getChildren());
673             request.setProperties(actualLocation, readNode.getProperties());
674 
675             if (includeChildrenInSubgraph(actualLocation, readNode.getPropertiesByName(), first)) {
676 
677                 // Add each of the children to the list of locations that we need to read ...
678                 for (Location child : readNode.getChildren()) {
679                     locationsToRead.add(new LocationWithDepth(child, read.depth + 1));
680                 }
681             }
682 
683             if (first) first = false;
684         }
685         setCacheableInfo(request);
686     }
687 
688     /**
689      * This method is called from {@link #process(ReadBranchRequest)} when determining the maximum depth for the subgraph. By
690      * default, this method returns {@link Integer#MAX_VALUE}, signaling that the ReadBranchRequest's
691      * {@link ReadBranchRequest#maximumDepth() maximum depth} should be honored. However, subclasses can override this method to
692      * return a constant value that will be used if less than the ReadBranchRequest's maximum depth.
693      * 
694      * @return the maximum read depth allowed by the processor; must be positive
695      */
696     protected int absoluteMaximumDepthForBranchReads() {
697         return Integer.MAX_VALUE;
698     }
699 
700     /**
701      * This method is called from {@link #process(ReadBranchRequest)} when determining whether particular nodes should be included
702      * in subgraph reads. For example, some processor implementations might want to always exclude nodes with certain names from
703      * all subgraph reads. If this is the case, subclasses should override this method (which always returns true), and determine
704      * whether the node at the supplied location should be included in the subgraph.
705      * 
706      * @param location the location of the parent node; never null
707      * @param properties the properties of the parent node; never null
708      * @param topOfSubgraph true if the parent node (identified by the location) is the root of the subgraph
709      * @return true if the child nodes should be read and included in the subgraph, or false otherwise
710      */
711     protected boolean includeChildrenInSubgraph( Location location,
712                                                  Map<Name, Property> properties,
713                                                  boolean topOfSubgraph ) {
714         return true;
715     }
716 
717     /**
718      * Process a request to read the properties of a node at the supplied location.
719      * <p>
720      * This method does nothing if the request is null.
721      * </p>
722      * 
723      * @param request the read request
724      */
725     public abstract void process( ReadAllPropertiesRequest request );
726 
727     /**
728      * Process a request to read the properties and children of a node at the supplied location.
729      * <p>
730      * This method does nothing if the request is null. Unless overridden, this method converts the single request into a
731      * {@link ReadAllChildrenRequest} and a {@link ReadAllPropertiesRequest}.
732      * </p>
733      * 
734      * @param request the read request
735      */
736     public void process( ReadNodeRequest request ) {
737         if (request == null) return;
738         // Read the properties ...
739         ReadAllPropertiesRequest readProperties = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
740         process(readProperties);
741         if (readProperties.hasError()) {
742             request.setError(readProperties.getError());
743             return;
744         }
745         // Set the actual location ...
746         request.setActualLocationOfNode(readProperties.getActualLocationOfNode());
747 
748         // Read the children ...
749         ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at(), request.inWorkspace());
750         process(readChildren);
751         if (readChildren.hasError()) {
752             request.setError(readChildren.getError());
753             return;
754         }
755         if (request.isCancelled()) return;
756         // Now, copy all of the results into the submitted request ...
757         for (Property property : readProperties) {
758             request.addProperty(property);
759         }
760         for (Location child : readChildren) {
761             request.addChild(child);
762         }
763         setCacheableInfo(request);
764     }
765 
766     /**
767      * Process a request to read a single property of a node at the supplied location.
768      * <p>
769      * This method does nothing if the request is null. Unless overridden, this method converts the request that
770      * {@link ReadAllPropertiesRequest reads the node} and simply returns the one property.
771      * </p>
772      * 
773      * @param request the read request
774      */
775     public void process( ReadPropertyRequest request ) {
776         if (request == null) return;
777         ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.on(), request.inWorkspace());
778         process(readNode);
779         if (readNode.hasError()) {
780             request.setError(readNode.getError());
781             return;
782         }
783         Property property = readNode.getPropertiesByName().get(request.named());
784         request.setProperty(property);
785         // Set the actual location ...
786         request.setActualLocationOfNode(readNode.getActualLocationOfNode());
787         setCacheableInfo(request);
788     }
789 
790     /**
791      * Process a request to verify that a node exists at the supplied location.
792      * <p>
793      * This method does nothing if the request is null. Unless overridden, this method converts the request that
794      * {@link ReadAllPropertiesRequest reads the node} and uses the result to determine if the node exists.
795      * </p>
796      * 
797      * @param request the read request
798      */
799     public void process( VerifyNodeExistsRequest request ) {
800         if (request == null) return;
801         ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
802         process(readNode);
803         if (readNode.hasError()) {
804             request.setError(readNode.getError());
805             return;
806         }
807         // Set the actual location ...
808         request.setActualLocationOfNode(readNode.getActualLocationOfNode());
809         setCacheableInfo(request);
810     }
811 
812     /**
813      * Process a request to remove the specified property from a node.
814      * <p>
815      * This method does nothing if the request is null. Unless overridden, this method converts this request into a
816      * {@link UpdatePropertiesRequest}.
817      * </p>
818      * 
819      * @param request the request to remove the property
820      */
821     public void process( RemovePropertyRequest request ) {
822         if (request == null) return;
823         Map<Name, Property> properties = Collections.singletonMap(request.propertyName(), null);
824         UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.from(), request.inWorkspace(), properties);
825         process(update);
826         if (update.hasError()) {
827             request.setError(update.getError());
828         }
829         // Set the actual location ...
830         request.setActualLocationOfNode(update.getActualLocationOfNode());
831     }
832 
833     /**
834      * Process a request to set the specified property on a node.
835      * <p>
836      * This method does nothing if the request is null. Unless overridden, this method converts this request into a
837      * {@link UpdatePropertiesRequest}.
838      * </p>
839      * 
840      * @param request the request to set the property
841      */
842     public void process( SetPropertyRequest request ) {
843         if (request == null) return;
844         Property property = request.property();
845         Map<Name, Property> properties = Collections.singletonMap(property.getName(), property);
846         UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.on(), request.inWorkspace(), properties);
847         process(update);
848         if (update.hasError()) {
849             request.setError(update.getError());
850         } else {
851             // Set the actual location and created flags ...
852             request.setActualLocationOfNode(update.getActualLocationOfNode());
853             request.setNewProperty(update.isNewProperty(property.getName()));
854         }
855     }
856 
857     /**
858      * Process a request to remove the specified properties from a node.
859      * <p>
860      * This method does nothing if the request is null.
861      * </p>
862      * 
863      * @param request the remove request
864      */
865     public abstract void process( UpdatePropertiesRequest request );
866 
867     /**
868      * Process a request to add and/or remove the specified values from a property on the given node.
869      * <p>
870      * This method does nothing if the request is null.
871      * </p>
872      * 
873      * @param request the remove request
874      */
875     public void process( UpdateValuesRequest request ) {
876         String workspaceName = request.inWorkspace();
877         Location on = request.on();
878         Name propertyName = request.property();
879 
880         // Read in the current values
881         ReadPropertyRequest readProperty = new ReadPropertyRequest(on, workspaceName, propertyName);
882         process(readProperty);
883 
884         if (readProperty.hasError()) {
885             request.setError(readProperty.getError());
886             return;
887         }
888 
889         Property property = readProperty.getProperty();
890         List<Object> actualRemovedValues = new ArrayList<Object>(request.removedValues().size());
891         List<Object> newValues = property == null ? new LinkedList<Object>() : new LinkedList<Object>(
892                                                                                                       Arrays.asList(property.getValuesAsArray()));
893         // Calculate what the new values should be
894         for (Object removedValue : request.removedValues()) {
895             for (Iterator<Object> iter = newValues.iterator(); iter.hasNext();) {
896                 if (iter.next().equals(removedValue)) {
897                     iter.remove();
898                     actualRemovedValues.add(removedValue);
899                     break;
900                 }
901             }
902         }
903 
904         newValues.addAll(request.addedValues());
905         Property newProperty = getExecutionContext().getPropertyFactory().create(propertyName, newValues);
906 
907         // Update the current values
908         SetPropertyRequest setProperty = new SetPropertyRequest(on, workspaceName, newProperty);
909         process(setProperty);
910 
911         if (setProperty.hasError()) {
912             request.setError(setProperty.getError());
913         } else {
914             // Set the actual location and property
915             request.setActualLocation(setProperty.getActualLocationOfNode(), request.addedValues(), actualRemovedValues);
916             request.setActualProperty(newProperty, setProperty.isNewProperty());
917         }
918 
919     }
920 
921     /**
922      * Process a request to rename a node specified location into a different location.
923      * <p>
924      * This method does nothing if the request is null. Unless overridden, this method converts the rename into a
925      * {@link MoveBranchRequest move}. However, this only works if the <code>request</code> has a {@link Location#hasPath() path}
926      * for its {@link RenameNodeRequest#at() location}. (If not, this method throws an {@link UnsupportedOperationException} and
927      * must be overridden.)
928      * </p>
929      * 
930      * @param request the rename request
931      */
932     public void process( RenameNodeRequest request ) {
933         if (request == null) return;
934         Location from = request.at();
935         if (!from.hasPath()) {
936             throw new UnsupportedOperationException();
937         }
938         Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(from.getPath(), request.toName());
939         Location to = Location.create(newPath);
940         MoveBranchRequest move = new MoveBranchRequest(from, to, request.inWorkspace());
941         process(move);
942         // Set the actual locations ...
943         request.setActualLocations(move.getActualLocationBefore(), move.getActualLocationAfter());
944     }
945 
946     /**
947      * Process a request to lock a node or branch within a workspace
948      * <p>
949      * The default implementation of this method does nothing, as most connectors will not support locking. Any implementation of
950      * this method should do nothing if the request is null.
951      * </p>
952      * <p>
953      * Implementations that do support locking should throw a {@link LockFailedException} if the request could not be fulfilled.
954      * </p>
955      * 
956      * @param request the request
957      */
958     public void process( LockBranchRequest request ) {
959         Location actualLocation = request.at();
960         if (!actualLocation.hasPath()) {
961             VerifyNodeExistsRequest nodeExists = new VerifyNodeExistsRequest(request.at(), request.inWorkspace());
962 
963             process(nodeExists);
964 
965             if (nodeExists.hasError()) {
966                 request.setError(nodeExists.getError());
967                 return;
968             }
969 
970             actualLocation = nodeExists.getActualLocationOfNode();
971         }
972 
973         request.setActualLocation(actualLocation);
974     }
975 
976     /**
977      * Process a request to unlock a node or branch within a workspace
978      * <p>
979      * The default implementation of this method does nothing, as most connectors will not support locking. Any implementation of
980      * this method should do nothing if the request is null.
981      * </p>
982      * 
983      * @param request the request
984      */
985     public void process( UnlockBranchRequest request ) {
986         Location actualLocation = request.at();
987         if (!actualLocation.hasPath()) {
988             VerifyNodeExistsRequest nodeExists = new VerifyNodeExistsRequest(request.at(), request.inWorkspace());
989 
990             process(nodeExists);
991 
992             if (nodeExists.hasError()) {
993                 request.setError(nodeExists.getError());
994                 return;
995             }
996 
997             actualLocation = nodeExists.getActualLocationOfNode();
998         }
999 
1000         request.setActualLocation(actualLocation);
1001     }
1002 
1003     /**
1004      * Process a request to query a workspace with an access query, which is is a low-level atomic query that is part of a larger,
1005      * planned query.
1006      * <p>
1007      * The default implementation of this method behaves as though the implementation does not support queries by setting an error
1008      * on the request
1009      * </p>
1010      * 
1011      * @param request the request
1012      */
1013     public void process( AccessQueryRequest request ) {
1014         processUnknownRequest(request);
1015     }
1016 
1017     /**
1018      * Process a request to search a workspace.
1019      * <p>
1020      * The default implementation of this method behaves as though the implementation does not support full-text searches by
1021      * setting an error on the request
1022      * </p>
1023      * 
1024      * @param request the request
1025      */
1026     public void process( FullTextSearchRequest request ) {
1027         processUnknownRequest(request);
1028     }
1029 
1030     /**
1031      * Process a request to collect garbage.
1032      * <p>
1033      * The default implementation of this method does nothing.
1034      * </p>
1035      * 
1036      * @param request the request
1037      */
1038     public void process( CollectGarbageRequest request ) {
1039         // do nothing by default
1040     }
1041 
1042     /**
1043      * Close this processor, allowing it to clean up any open resources.
1044      */
1045     public void close() {
1046         // do nothing by default
1047     }
1048 
1049     /**
1050      * Obtain the list of {@link ChangeRequest}s that were successfully processed by this processor.
1051      * <p>
1052      * Note that this list is modified during processing and thus should only be accessed by the caller when this processor has
1053      * been {@link #close() closed}.
1054      * </p>
1055      * <p>
1056      * Also, if this processor encounters errors while processing {@link Request#isReadOnly() change requests}, the processor does
1057      * not throw out any of the changes. Thus it is up to the caller to decide whether any of the changes are to be kept.
1058      * </p>
1059      * 
1060      * @return the list of successful changes; never null but possibly empty.
1061      */
1062     public List<ChangeRequest> getChanges() {
1063         return changes;
1064     }
1065 
1066     /**
1067      * Take any of the changes that have been accumulated by this processor and notify the observer. This should only be called
1068      * after {@link #close()} has been called.
1069      */
1070     public void notifyObserverOfChanges() {
1071         if (observer == null) {
1072             if (changes != null) changes.clear();
1073             return;
1074         }
1075         if (this.changes.isEmpty()) return;
1076         // then publish any changes ...
1077         String userName = context.getSecurityContext() != null ? context.getSecurityContext().getUserName() : null;
1078         if (userName == null) userName = "";
1079         String contextId = context.getId();
1080         String processId = context.getProcessId();
1081         Map<String, String> userData = context.getData();
1082         Changes changes = new Changes(processId, contextId, userName, getSourceName(), getNowInUtc(), this.changes, userData);
1083         observer.notify(changes);
1084         // Null the list, since this should have been closed
1085         this.changes = null;
1086     }
1087 
1088     /**
1089      * A class that represents a location at a known depth
1090      * 
1091      * @author Randall Hauch
1092      */
1093     @Immutable
1094     protected class LocationWithDepth {
1095         protected final Location location;
1096         protected final int depth;
1097 
1098         public LocationWithDepth( Location location,
1099                                   int depth ) {
1100             this.location = location;
1101             this.depth = depth;
1102         }
1103 
1104         public Location getLocation() {
1105             return location;
1106         }
1107 
1108         public int getDepth() {
1109             return depth;
1110         }
1111 
1112         @Override
1113         public int hashCode() {
1114             return location.hashCode();
1115         }
1116 
1117         @Override
1118         public String toString() {
1119             return location.toString() + " at depth " + depth;
1120         }
1121     }
1122 
1123 }