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 
643         // Now read the locations ...
644         boolean first = true;
645         while (locationsToRead.peek() != null) {
646             if (request.isCancelled()) return;
647             LocationWithDepth read = locationsToRead.poll();
648 
649             // Check the depth ...
650             if (read.depth > request.maximumDepth()) break;
651 
652             // Read the properties ...
653             ReadNodeRequest readNode = new ReadNodeRequest(read.location, request.inWorkspace());
654             process(readNode);
655             if (readNode.hasError()) {
656                 request.setError(readNode.getError());
657                 return;
658             }
659             Location actualLocation = readNode.getActualLocationOfNode();
660             if (first) {
661                 // Set the actual location on the original request
662                 request.setActualLocationOfNode(actualLocation);
663                 first = false;
664             }
665 
666             // Record in the request the children and properties that were read on this node ...
667             request.setChildren(actualLocation, readNode.getChildren());
668             request.setProperties(actualLocation, readNode.getProperties());
669 
670             // Add each of the children to the list of locations that we need to read ...
671             for (Location child : readNode.getChildren()) {
672                 locationsToRead.add(new LocationWithDepth(child, read.depth + 1));
673             }
674         }
675         setCacheableInfo(request);
676     }
677 
678     /**
679      * Process a request to read the properties of a node at the supplied location.
680      * <p>
681      * This method does nothing if the request is null.
682      * </p>
683      * 
684      * @param request the read request
685      */
686     public abstract void process( ReadAllPropertiesRequest request );
687 
688     /**
689      * Process a request to read the properties and children of a node at the supplied location.
690      * <p>
691      * This method does nothing if the request is null. Unless overridden, this method converts the single request into a
692      * {@link ReadAllChildrenRequest} and a {@link ReadAllPropertiesRequest}.
693      * </p>
694      * 
695      * @param request the read request
696      */
697     public void process( ReadNodeRequest request ) {
698         if (request == null) return;
699         // Read the properties ...
700         ReadAllPropertiesRequest readProperties = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
701         process(readProperties);
702         if (readProperties.hasError()) {
703             request.setError(readProperties.getError());
704             return;
705         }
706         // Set the actual location ...
707         request.setActualLocationOfNode(readProperties.getActualLocationOfNode());
708 
709         // Read the children ...
710         ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at(), request.inWorkspace());
711         process(readChildren);
712         if (readChildren.hasError()) {
713             request.setError(readChildren.getError());
714             return;
715         }
716         if (request.isCancelled()) return;
717         // Now, copy all of the results into the submitted request ...
718         for (Property property : readProperties) {
719             request.addProperty(property);
720         }
721         for (Location child : readChildren) {
722             request.addChild(child);
723         }
724         setCacheableInfo(request);
725     }
726 
727     /**
728      * Process a request to read a single property 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 request that
731      * {@link ReadAllPropertiesRequest reads the node} and simply returns the one property.
732      * </p>
733      * 
734      * @param request the read request
735      */
736     public void process( ReadPropertyRequest request ) {
737         if (request == null) return;
738         ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.on(), request.inWorkspace());
739         process(readNode);
740         if (readNode.hasError()) {
741             request.setError(readNode.getError());
742             return;
743         }
744         Property property = readNode.getPropertiesByName().get(request.named());
745         request.setProperty(property);
746         // Set the actual location ...
747         request.setActualLocationOfNode(readNode.getActualLocationOfNode());
748         setCacheableInfo(request);
749     }
750 
751     /**
752      * Process a request to verify that a node exists at the supplied location.
753      * <p>
754      * This method does nothing if the request is null. Unless overridden, this method converts the request that
755      * {@link ReadAllPropertiesRequest reads the node} and uses the result to determine if the node exists.
756      * </p>
757      * 
758      * @param request the read request
759      */
760     public void process( VerifyNodeExistsRequest request ) {
761         if (request == null) return;
762         ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
763         process(readNode);
764         if (readNode.hasError()) {
765             request.setError(readNode.getError());
766             return;
767         }
768         // Set the actual location ...
769         request.setActualLocationOfNode(readNode.getActualLocationOfNode());
770         setCacheableInfo(request);
771     }
772 
773     /**
774      * Process a request to remove the specified property from a node.
775      * <p>
776      * This method does nothing if the request is null. Unless overridden, this method converts this request into a
777      * {@link UpdatePropertiesRequest}.
778      * </p>
779      * 
780      * @param request the request to remove the property
781      */
782     public void process( RemovePropertyRequest request ) {
783         if (request == null) return;
784         Map<Name, Property> properties = Collections.singletonMap(request.propertyName(), null);
785         UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.from(), request.inWorkspace(), properties);
786         process(update);
787         if (update.hasError()) {
788             request.setError(update.getError());
789         }
790         // Set the actual location ...
791         request.setActualLocationOfNode(update.getActualLocationOfNode());
792     }
793 
794     /**
795      * Process a request to set the specified property on a node.
796      * <p>
797      * This method does nothing if the request is null. Unless overridden, this method converts this request into a
798      * {@link UpdatePropertiesRequest}.
799      * </p>
800      * 
801      * @param request the request to set the property
802      */
803     public void process( SetPropertyRequest request ) {
804         if (request == null) return;
805         Property property = request.property();
806         Map<Name, Property> properties = Collections.singletonMap(property.getName(), property);
807         UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.on(), request.inWorkspace(), properties);
808         process(update);
809         if (update.hasError()) {
810             request.setError(update.getError());
811         } else {
812             // Set the actual location and created flags ...
813             request.setActualLocationOfNode(update.getActualLocationOfNode());
814             request.setNewProperty(update.isNewProperty(property.getName()));
815         }
816     }
817 
818     /**
819      * Process a request to remove the specified properties from a node.
820      * <p>
821      * This method does nothing if the request is null.
822      * </p>
823      * 
824      * @param request the remove request
825      */
826     public abstract void process( UpdatePropertiesRequest request );
827 
828     /**
829      * Process a request to add and/or remove the specified values from a property on the given node.
830      * <p>
831      * This method does nothing if the request is null.
832      * </p>
833      * 
834      * @param request the remove request
835      */
836     public void process( UpdateValuesRequest request ) {
837         String workspaceName = request.inWorkspace();
838         Location on = request.on();
839         Name propertyName = request.property();
840 
841         // Read in the current values
842         ReadPropertyRequest readProperty = new ReadPropertyRequest(on, workspaceName, propertyName);
843         process(readProperty);
844 
845         if (readProperty.hasError()) {
846             request.setError(readProperty.getError());
847             return;
848         }
849 
850         Property property = readProperty.getProperty();
851         List<Object> actualRemovedValues = new ArrayList<Object>(request.removedValues().size());
852         List<Object> newValues = property == null ? new LinkedList<Object>() : new LinkedList<Object>(
853                                                                                                       Arrays.asList(property.getValuesAsArray()));
854         // Calculate what the new values should be
855         for (Object removedValue : request.removedValues()) {
856             for (Iterator<Object> iter = newValues.iterator(); iter.hasNext();) {
857                 if (iter.next().equals(removedValue)) {
858                     iter.remove();
859                     actualRemovedValues.add(removedValue);
860                     break;
861                 }
862             }
863         }
864 
865         newValues.addAll(request.addedValues());
866         Property newProperty = getExecutionContext().getPropertyFactory().create(propertyName, newValues);
867 
868         // Update the current values
869         SetPropertyRequest setProperty = new SetPropertyRequest(on, workspaceName, newProperty);
870         process(setProperty);
871 
872         if (setProperty.hasError()) {
873             request.setError(setProperty.getError());
874         } else {
875             // Set the actual location and property
876             request.setActualLocation(setProperty.getActualLocationOfNode(), request.addedValues(), actualRemovedValues);
877             request.setActualProperty(newProperty, setProperty.isNewProperty());
878         }
879 
880     }
881 
882     /**
883      * Process a request to rename a node specified location into a different location.
884      * <p>
885      * This method does nothing if the request is null. Unless overridden, this method converts the rename into a
886      * {@link MoveBranchRequest move}. However, this only works if the <code>request</code> has a {@link Location#hasPath() path}
887      * for its {@link RenameNodeRequest#at() location}. (If not, this method throws an {@link UnsupportedOperationException} and
888      * must be overridden.)
889      * </p>
890      * 
891      * @param request the rename request
892      */
893     public void process( RenameNodeRequest request ) {
894         if (request == null) return;
895         Location from = request.at();
896         if (!from.hasPath()) {
897             throw new UnsupportedOperationException();
898         }
899         Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(from.getPath(), request.toName());
900         Location to = Location.create(newPath);
901         MoveBranchRequest move = new MoveBranchRequest(from, to, request.inWorkspace());
902         process(move);
903         // Set the actual locations ...
904         request.setActualLocations(move.getActualLocationBefore(), move.getActualLocationAfter());
905     }
906 
907     /**
908      * Process a request to lock a node or branch within a workspace
909      * <p>
910      * The default implementation of this method does nothing, as most connectors will not support locking. Any implementation of
911      * this method should do nothing if the request is null.
912      * </p>
913      * <p>
914      * Implementations that do support locking should throw a {@link LockFailedException} if the request could not be fulfilled.
915      * </p>
916      * 
917      * @param request the request
918      */
919     public void process( LockBranchRequest request ) {
920         Location actualLocation = request.at();
921         if (!actualLocation.hasPath()) {
922             VerifyNodeExistsRequest nodeExists = new VerifyNodeExistsRequest(request.at(), request.inWorkspace());
923 
924             process(nodeExists);
925 
926             if (nodeExists.hasError()) {
927                 request.setError(nodeExists.getError());
928                 return;
929             }
930 
931             actualLocation = nodeExists.getActualLocationOfNode();
932         }
933 
934         request.setActualLocation(actualLocation);
935     }
936 
937     /**
938      * Process a request to unlock a node or branch within a workspace
939      * <p>
940      * The default implementation of this method does nothing, as most connectors will not support locking. Any implementation of
941      * this method should do nothing if the request is null.
942      * </p>
943      * 
944      * @param request the request
945      */
946     public void process( UnlockBranchRequest request ) {
947         Location actualLocation = request.at();
948         if (!actualLocation.hasPath()) {
949             VerifyNodeExistsRequest nodeExists = new VerifyNodeExistsRequest(request.at(), request.inWorkspace());
950 
951             process(nodeExists);
952 
953             if (nodeExists.hasError()) {
954                 request.setError(nodeExists.getError());
955                 return;
956             }
957 
958             actualLocation = nodeExists.getActualLocationOfNode();
959         }
960 
961         request.setActualLocation(actualLocation);
962     }
963 
964     /**
965      * 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,
966      * planned query.
967      * <p>
968      * The default implementation of this method behaves as though the implementation does not support queries by setting an error
969      * on the request
970      * </p>
971      * 
972      * @param request the request
973      */
974     public void process( AccessQueryRequest request ) {
975         processUnknownRequest(request);
976     }
977 
978     /**
979      * Process a request to search a workspace.
980      * <p>
981      * The default implementation of this method behaves as though the implementation does not support full-text searches by
982      * setting an error on the request
983      * </p>
984      * 
985      * @param request the request
986      */
987     public void process( FullTextSearchRequest request ) {
988         processUnknownRequest(request);
989     }
990 
991     /**
992      * Close this processor, allowing it to clean up any open resources.
993      */
994     public void close() {
995         // do nothing by default
996     }
997 
998     /**
999      * Obtain the list of {@link ChangeRequest}s that were successfully processed by this processor.
1000      * <p>
1001      * Note that this list is modified during processing and thus should only be accessed by the caller when this processor has
1002      * been {@link #close() closed}.
1003      * </p>
1004      * <p>
1005      * Also, if this processor encounters errors while processing {@link Request#isReadOnly() change requests}, the processor does
1006      * 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.
1007      * </p>
1008      * 
1009      * @return the list of successful changes; never null but possibly empty.
1010      */
1011     public List<ChangeRequest> getChanges() {
1012         return changes;
1013     }
1014 
1015     /**
1016      * Take any of the changes that have been accumulated by this processor and notify the observer. This should only be called
1017      * after {@link #close()} has been called.
1018      */
1019     public void notifyObserverOfChanges() {
1020         if (observer == null) {
1021             if (changes != null) changes.clear();
1022             return;
1023         }
1024         if (this.changes.isEmpty()) return;
1025         // then publish any changes ...
1026         String userName = context.getSecurityContext() != null ? context.getSecurityContext().getUserName() : null;
1027         if (userName == null) userName = "";
1028         String contextId = context.getId();
1029         String processId = null;
1030         Map<String, String> userData = context.getData();
1031         Changes changes = new Changes(processId, contextId, userName, getSourceName(), getNowInUtc(), this.changes, userData);
1032         observer.notify(changes);
1033         // Null the list, since this should have been closed
1034         this.changes = null;
1035     }
1036 
1037     /**
1038      * A class that represents a location at a known depth
1039      * 
1040      * @author Randall Hauch
1041      */
1042     @Immutable
1043     protected class LocationWithDepth {
1044         protected final Location location;
1045         protected final int depth;
1046 
1047         public LocationWithDepth( Location location,
1048                                   int depth ) {
1049             this.location = location;
1050             this.depth = depth;
1051         }
1052 
1053         public Location getLocation() {
1054             return location;
1055         }
1056 
1057         public int getDepth() {
1058             return depth;
1059         }
1060 
1061         @Override
1062         public int hashCode() {
1063             return location.hashCode();
1064         }
1065 
1066         @Override
1067         public String toString() {
1068             return location.toString() + " at depth " + depth;
1069         }
1070     }
1071 
1072 }