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