001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.graph.request.processor;
025    
026    import java.util.Collections;
027    import java.util.LinkedList;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.Queue;
031    import net.jcip.annotations.Immutable;
032    import org.jboss.dna.common.util.CheckArg;
033    import org.jboss.dna.graph.ExecutionContext;
034    import org.jboss.dna.graph.GraphI18n;
035    import org.jboss.dna.graph.Location;
036    import org.jboss.dna.graph.cache.CachePolicy;
037    import org.jboss.dna.graph.connector.RepositorySourceException;
038    import org.jboss.dna.graph.observe.Changes;
039    import org.jboss.dna.graph.observe.Observer;
040    import org.jboss.dna.graph.property.DateTime;
041    import org.jboss.dna.graph.property.Name;
042    import org.jboss.dna.graph.property.Path;
043    import org.jboss.dna.graph.property.Property;
044    import org.jboss.dna.graph.property.ReferentialIntegrityException;
045    import org.jboss.dna.graph.request.CacheableRequest;
046    import org.jboss.dna.graph.request.ChangeRequest;
047    import org.jboss.dna.graph.request.CloneWorkspaceRequest;
048    import org.jboss.dna.graph.request.CompositeRequest;
049    import org.jboss.dna.graph.request.CopyBranchRequest;
050    import org.jboss.dna.graph.request.CreateNodeRequest;
051    import org.jboss.dna.graph.request.CreateWorkspaceRequest;
052    import org.jboss.dna.graph.request.DeleteBranchRequest;
053    import org.jboss.dna.graph.request.DeleteChildrenRequest;
054    import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
055    import org.jboss.dna.graph.request.GetWorkspacesRequest;
056    import org.jboss.dna.graph.request.InvalidRequestException;
057    import org.jboss.dna.graph.request.MoveBranchRequest;
058    import org.jboss.dna.graph.request.ReadAllChildrenRequest;
059    import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
060    import org.jboss.dna.graph.request.ReadBlockOfChildrenRequest;
061    import org.jboss.dna.graph.request.ReadBranchRequest;
062    import org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest;
063    import org.jboss.dna.graph.request.ReadNodeRequest;
064    import org.jboss.dna.graph.request.ReadPropertyRequest;
065    import org.jboss.dna.graph.request.RemovePropertyRequest;
066    import org.jboss.dna.graph.request.RenameNodeRequest;
067    import org.jboss.dna.graph.request.Request;
068    import org.jboss.dna.graph.request.SetPropertyRequest;
069    import org.jboss.dna.graph.request.UnsupportedRequestException;
070    import org.jboss.dna.graph.request.UpdatePropertiesRequest;
071    import org.jboss.dna.graph.request.VerifyNodeExistsRequest;
072    import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
073    
074    /**
075     * A component that is used to process and execute {@link Request}s. This class is intended to be subclassed and methods
076     * overwritten to define the behavior for executing the different kinds of requests. Abstract methods must be overridden, but
077     * non-abstract methods all have meaningful default implementations.
078     * 
079     * @author Randall Hauch
080     */
081    @Immutable
082    public abstract class RequestProcessor {
083    
084        private final ExecutionContext context;
085        private final String sourceName;
086        private final DateTime nowInUtc;
087        private final CachePolicy defaultCachePolicy;
088        private final List<ChangeRequest> changes;
089        private final Observer observer;
090    
091        protected RequestProcessor( String sourceName,
092                                    ExecutionContext context,
093                                    Observer observer ) {
094            this(sourceName, context, observer, null, null);
095        }
096    
097        protected RequestProcessor( String sourceName,
098                                    ExecutionContext context,
099                                    Observer observer,
100                                    DateTime now ) {
101            this(sourceName, context, observer, now, null);
102        }
103    
104        protected RequestProcessor( String sourceName,
105                                    ExecutionContext context,
106                                    Observer observer,
107                                    DateTime now,
108                                    CachePolicy defaultCachePolicy ) {
109            CheckArg.isNotEmpty(sourceName, "sourceName");
110            CheckArg.isNotNull(context, "context");
111            this.context = context;
112            this.sourceName = sourceName;
113            this.nowInUtc = now != null ? now : context.getValueFactories().getDateFactory().createUtc();
114            this.defaultCachePolicy = defaultCachePolicy;
115            this.changes = observer != null ? new LinkedList<ChangeRequest>() : null;
116            this.observer = observer;
117        }
118    
119        /**
120         * Record the supplied change request for publishing through the event mechanism.
121         * 
122         * @param request the completed change request; may not be null
123         */
124        protected void recordChange( ChangeRequest request ) {
125            assert request != null;
126            assert !request.isCancelled();
127            assert !request.hasError();
128            if (changes != null) changes.add(request);
129        }
130    
131        /**
132         * Get the name of the source against which this processor is executing.
133         * 
134         * @return the repository source name; never null or empty
135         */
136        public final String getSourceName() {
137            return sourceName;
138        }
139    
140        /**
141         * The execution context that this process is operating within.
142         * 
143         * @return the execution context; never null
144         */
145        public final ExecutionContext getExecutionContext() {
146            return this.context;
147        }
148    
149        /**
150         * Get the 'current time' for this processor, which is usually a constant during its lifetime.
151         * 
152         * @return the current time in UTC; never null
153         */
154        protected final DateTime getNowInUtc() {
155            return this.nowInUtc;
156        }
157    
158        /**
159         * @return defaultCachePolicy
160         */
161        protected final CachePolicy getDefaultCachePolicy() {
162            return defaultCachePolicy;
163        }
164    
165        /**
166         * Set the supplied request to have the default cache policy and the {@link #getNowInUtc() current time in UTC}.
167         * 
168         * @param request the cacheable request
169         */
170        protected void setCacheableInfo( CacheableRequest request ) {
171            // Set it only if the request has no cache policy already ...
172            if (request.getCachePolicy() == null && defaultCachePolicy != null) {
173                request.setCachePolicy(defaultCachePolicy);
174            }
175            request.setTimeLoaded(nowInUtc);
176        }
177    
178        /**
179         * Set the supplied request to have the supplied cache policy and the {@link #getNowInUtc() current time in UTC}.
180         * 
181         * @param request the cacheable request
182         * @param cachePolicy the cache policy for the request; may be null if there is to be no cache policy
183         */
184        protected void setCacheableInfo( CacheableRequest request,
185                                         CachePolicy cachePolicy ) {
186            if (cachePolicy == null) cachePolicy = defaultCachePolicy;
187            if (cachePolicy != null) {
188                if (request.getCachePolicy() != null) {
189                    // Set the supplied only if less than the current ...
190                    if (request.getCachePolicy().getTimeToLive() > cachePolicy.getTimeToLive()) {
191                        request.setCachePolicy(cachePolicy);
192                    }
193                } else {
194                    // There is no current policy, so set the supplied policy ...
195                    request.setCachePolicy(cachePolicy);
196                }
197            }
198            request.setTimeLoaded(nowInUtc);
199        }
200    
201        /**
202         * Process a request by determining the type of request and delegating to the appropriate <code>process</code> method for that
203         * type.
204         * <p>
205         * This method does nothing if the request is null.
206         * </p>
207         * 
208         * @param request the general request
209         */
210        public void process( Request request ) {
211            if (request == null) return;
212            try {
213                if (request.isCancelled()) return;
214                if (request instanceof CompositeRequest) {
215                    process((CompositeRequest)request);
216                } else if (request instanceof CopyBranchRequest) {
217                    process((CopyBranchRequest)request);
218                } else if (request instanceof CreateNodeRequest) {
219                    process((CreateNodeRequest)request);
220                } else if (request instanceof DeleteBranchRequest) {
221                    process((DeleteBranchRequest)request);
222                } else if (request instanceof DeleteChildrenRequest) {
223                    process((DeleteChildrenRequest)request);
224                } else if (request instanceof MoveBranchRequest) {
225                    process((MoveBranchRequest)request);
226                } else if (request instanceof ReadAllChildrenRequest) {
227                    process((ReadAllChildrenRequest)request);
228                } else if (request instanceof ReadNextBlockOfChildrenRequest) {
229                    process((ReadNextBlockOfChildrenRequest)request);
230                } else if (request instanceof ReadBlockOfChildrenRequest) {
231                    process((ReadBlockOfChildrenRequest)request);
232                } else if (request instanceof ReadBranchRequest) {
233                    process((ReadBranchRequest)request);
234                } else if (request instanceof ReadNodeRequest) {
235                    process((ReadNodeRequest)request);
236                } else if (request instanceof ReadAllPropertiesRequest) {
237                    process((ReadAllPropertiesRequest)request);
238                } else if (request instanceof ReadPropertyRequest) {
239                    process((ReadPropertyRequest)request);
240                } else if (request instanceof RemovePropertyRequest) {
241                    process((RemovePropertyRequest)request);
242                } else if (request instanceof SetPropertyRequest) {
243                    process((SetPropertyRequest)request);
244                } else if (request instanceof RenameNodeRequest) {
245                    process((RenameNodeRequest)request);
246                } else if (request instanceof UpdatePropertiesRequest) {
247                    process((UpdatePropertiesRequest)request);
248                } else if (request instanceof VerifyNodeExistsRequest) {
249                    process((VerifyNodeExistsRequest)request);
250                } else if (request instanceof VerifyWorkspaceRequest) {
251                    process((VerifyWorkspaceRequest)request);
252                } else if (request instanceof GetWorkspacesRequest) {
253                    process((GetWorkspacesRequest)request);
254                } else if (request instanceof CreateWorkspaceRequest) {
255                    process((CreateWorkspaceRequest)request);
256                } else if (request instanceof CloneWorkspaceRequest) {
257                    process((CloneWorkspaceRequest)request);
258                } else if (request instanceof DestroyWorkspaceRequest) {
259                    process((DestroyWorkspaceRequest)request);
260                } else {
261                    processUnknownRequest(request);
262                }
263            } finally {
264                completeRequest(request);
265            }
266        }
267    
268        protected void completeRequest( Request request ) {
269            request.freeze();
270        }
271    
272        /**
273         * Process a request that is composed of multiple other (non-composite) requests. If any of the embedded requests
274         * {@link Request#hasError() has an error} after it is processed, the submitted request will be marked with an error.
275         * <p>
276         * This method does nothing if the request is null.
277         * </p>
278         * 
279         * @param request the composite request
280         */
281        public void process( CompositeRequest request ) {
282            if (request == null) return;
283            int numberOfErrors = 0;
284            List<Throwable> errors = null;
285            // Iterate over the requests in this composite, but only iterate once so that
286            for (Request embedded : request) {
287                assert embedded != null;
288                if (embedded.isCancelled()) return;
289                process(embedded);
290                if (embedded.hasError()) {
291                    if (numberOfErrors == 0) {
292                        errors = new LinkedList<Throwable>();
293                    }
294                    assert errors != null;
295                    errors.add(embedded.getError());
296                    ++numberOfErrors;
297                }
298            }
299            if (numberOfErrors == 0) return;
300            assert errors != null;
301            if (numberOfErrors == 1) {
302                request.setError(errors.get(0));
303            } else {
304                StringBuilder errorString = new StringBuilder();
305                for (Throwable error : errors) {
306                    errorString.append("\n");
307                    errorString.append("\t" + error.getMessage());
308                }
309                String msg = null;
310                if (request.size() == CompositeRequest.UNKNOWN_NUMBER_OF_REQUESTS) {
311                    msg = GraphI18n.multipleErrorsWhileExecutingManyRequests.text(numberOfErrors, errorString.toString());
312                } else {
313                    msg = GraphI18n.multipleErrorsWhileExecutingRequests.text(numberOfErrors, request.size(), errorString.toString());
314                }
315                request.setError(new RepositorySourceException(getSourceName(), msg));
316            }
317        }
318    
319        /**
320         * Method that is called by {@link #process(Request)} when the request was found to be of a request type that is not known by
321         * this processor. By default this method sets an {@link UnsupportedRequestException unsupported request error} on the
322         * request.
323         * 
324         * @param request the unknown request
325         */
326        protected void processUnknownRequest( Request request ) {
327            request.setError(new InvalidRequestException(GraphI18n.unsupportedRequestType.text(request.getClass().getName(), request)));
328        }
329    
330        /**
331         * Process a request to verify a named workspace.
332         * <p>
333         * This method does nothing if the request is null.
334         * </p>
335         * 
336         * @param request the request
337         */
338        public abstract void process( VerifyWorkspaceRequest request );
339    
340        /**
341         * Process a request to get the information about the available workspaces.
342         * <p>
343         * This method does nothing if the request is null.
344         * </p>
345         * 
346         * @param request the request
347         */
348        public abstract void process( GetWorkspacesRequest request );
349    
350        /**
351         * Process a request to create a new workspace.
352         * <p>
353         * This method does nothing if the request is null.
354         * </p>
355         * 
356         * @param request the request
357         */
358        public abstract void process( CreateWorkspaceRequest request );
359    
360        /**
361         * Process a request to clone an existing workspace as a new workspace.
362         * <p>
363         * This method does nothing if the request is null.
364         * </p>
365         * 
366         * @param request the request
367         */
368        public abstract void process( CloneWorkspaceRequest request );
369    
370        /**
371         * Process a request to permanently destroy a workspace.
372         * <p>
373         * This method does nothing if the request is null.
374         * </p>
375         * 
376         * @param request the request
377         */
378        public abstract void process( DestroyWorkspaceRequest request );
379    
380        /**
381         * Process a request to copy a branch into another location.
382         * <p>
383         * This method does nothing if the request is null.
384         * </p>
385         * 
386         * @param request the copy request
387         */
388        public abstract void process( CopyBranchRequest request );
389    
390        /**
391         * Process a request to create a node at a specified location.
392         * <p>
393         * This method does nothing if the request is null.
394         * </p>
395         * 
396         * @param request the create request
397         */
398        public abstract void process( CreateNodeRequest request );
399    
400        /**
401         * Process a request to delete a branch at a specified location.
402         * <p>
403         * This method does nothing if the request is null.
404         * </p>
405         * 
406         * @param request the delete request
407         * @throws ReferentialIntegrityException if the delete could not be performed because some references to deleted nodes would
408         *         have remained after the delete operation completed
409         */
410        public abstract void process( DeleteBranchRequest request );
411    
412        /**
413         * Process a request to delete all of the child nodes under the supplied existing node.
414         * <p>
415         * This method does nothing if the request is null.
416         * </p>
417         * 
418         * @param request the delete request
419         * @throws ReferentialIntegrityException if the delete could not be performed because some references to deleted nodes would
420         *         have remained after the delete operation completed
421         */
422        public void process( DeleteChildrenRequest request ) {
423            if (request == null) return;
424            if (request.isCancelled()) return;
425            // First get all of the children under the node ...
426            ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at(), request.inWorkspace());
427            process(readChildren);
428            if (readChildren.hasError()) {
429                request.setError(readChildren.getError());
430                return;
431            }
432            if (readChildren.isCancelled()) return;
433    
434            // Issue a DeleteBranchRequest for each child ...
435            for (Location child : readChildren) {
436                if (request.isCancelled()) return;
437                DeleteBranchRequest deleteChild = new DeleteBranchRequest(child, request.inWorkspace());
438                process(deleteChild);
439                request.addDeletedChild(child);
440            }
441    
442            // Set the actual location of the parent node ...
443            request.setActualLocationOfNode(readChildren.getActualLocationOfNode());
444        }
445    
446        /**
447         * Process a request to move a branch at a specified location into a different location.
448         * <p>
449         * This method does nothing if the request is null.
450         * </p>
451         * 
452         * @param request the move request
453         */
454        public abstract void process( MoveBranchRequest request );
455    
456        /**
457         * Process a request to read all of the children of a node.
458         * <p>
459         * This method does nothing if the request is null.
460         * </p>
461         * 
462         * @param request the read request
463         */
464        public abstract void process( ReadAllChildrenRequest request );
465    
466        /**
467         * Process a request to read a block of the children of a node. The block is defined by a
468         * {@link ReadBlockOfChildrenRequest#startingAtIndex() starting index} and a {@link ReadBlockOfChildrenRequest#count() maximum
469         * number of children to include in the block}.
470         * <p>
471         * This method does nothing if the request is null. The default implementation converts the command to a
472         * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
473         * implementation may not be efficient and may need to be overridden.
474         * </p>
475         * 
476         * @param request the read request
477         */
478        public void process( ReadBlockOfChildrenRequest request ) {
479            if (request == null) return;
480            // Convert the request to a ReadAllChildrenRequest and execute it ...
481            ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(request.of(), request.inWorkspace());
482            process(readAll);
483            if (readAll.hasError()) {
484                request.setError(readAll.getError());
485                return;
486            }
487            List<Location> allChildren = readAll.getChildren();
488    
489            // If there aren't enough children for the block's range ...
490            if (allChildren.size() < request.startingAtIndex()) return;
491    
492            // Now, find the children in the block ...
493            int endIndex = Math.min(request.endingBefore(), allChildren.size());
494            for (int i = request.startingAtIndex(); i != endIndex; ++i) {
495                request.addChild(allChildren.get(i));
496            }
497            // Set the actual location ...
498            request.setActualLocationOfNode(readAll.getActualLocationOfNode());
499            setCacheableInfo(request);
500        }
501    
502        /**
503         * Process a request to read the next block of the children of a node, starting after a previously-retrieved child.
504         * <p>
505         * This method does nothing if the request is null. The default implementation converts the command to a
506         * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
507         * implementation may not be efficient and may need to be overridden.
508         * </p>
509         * 
510         * @param request the read request
511         */
512        public void process( ReadNextBlockOfChildrenRequest request ) {
513            if (request == null) return;
514    
515            // Get the parent path ...
516            Location actualSiblingLocation = request.startingAfter();
517            Path path = actualSiblingLocation.getPath();
518            Path parentPath = null;
519            if (path != null) parentPath = path.getParent();
520            if (parentPath == null) {
521                // Need to find the parent path, so get the actual location of the sibling ...
522                VerifyNodeExistsRequest verifySibling = new VerifyNodeExistsRequest(request.startingAfter(), request.inWorkspace());
523                process(verifySibling);
524                actualSiblingLocation = verifySibling.getActualLocationOfNode();
525                parentPath = actualSiblingLocation.getPath().getParent();
526            }
527            assert parentPath != null;
528    
529            // Convert the request to a ReadAllChildrenRequest and execute it ...
530            ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(Location.create(parentPath), request.inWorkspace());
531            process(readAll);
532            if (readAll.hasError()) {
533                request.setError(readAll.getError());
534                return;
535            }
536            List<Location> allChildren = readAll.getChildren();
537    
538            // Iterate through the children, looking for the 'startingAfter' child ...
539            boolean found = false;
540            int count = 0;
541            for (Location child : allChildren) {
542                if (count > request.count()) break;
543                if (!found) {
544                    // Set to true if we find the child we're looking for ...
545                    found = child.equals(request.startingAfter());
546                } else {
547                    // Add the child to the block ...
548                    ++count;
549                    request.addChild(child);
550                }
551            }
552    
553            // Set the actual location ...
554            request.setActualLocationOfStartingAfterNode(actualSiblingLocation);
555            setCacheableInfo(request);
556        }
557    
558        /**
559         * Process a request to read a branch or subgraph that's below a node at a specified location.
560         * <p>
561         * This method does nothing if the request is null. The default implementation processes the branch by submitting the
562         * equivalent requests to {@link ReadNodeRequest read the nodes} and the {@link ReadAllChildrenRequest children}. It starts by
563         * doing this for the top-level node, then proceeds for each of the children of that node, and so forth.
564         * </p>
565         * 
566         * @param request the request to read the branch
567         */
568        public void process( ReadBranchRequest request ) {
569            if (request == null) return;
570            // Create a queue for locations that need to be read ...
571            Queue<LocationWithDepth> locationsToRead = new LinkedList<LocationWithDepth>();
572            locationsToRead.add(new LocationWithDepth(request.at(), 1));
573    
574            // Now read the locations ...
575            boolean first = true;
576            while (locationsToRead.peek() != null) {
577                if (request.isCancelled()) return;
578                LocationWithDepth read = locationsToRead.poll();
579    
580                // Check the depth ...
581                if (read.depth > request.maximumDepth()) break;
582    
583                // Read the properties ...
584                ReadNodeRequest readNode = new ReadNodeRequest(read.location, request.inWorkspace());
585                process(readNode);
586                if (readNode.hasError()) {
587                    request.setError(readNode.getError());
588                    return;
589                }
590                Location actualLocation = readNode.getActualLocationOfNode();
591                if (first) {
592                    // Set the actual location on the original request
593                    request.setActualLocationOfNode(actualLocation);
594                    first = false;
595                }
596    
597                // Record in the request the children and properties that were read on this node ...
598                request.setChildren(actualLocation, readNode.getChildren());
599                request.setProperties(actualLocation, readNode.getProperties());
600    
601                // Add each of the children to the list of locations that we need to read ...
602                for (Location child : readNode.getChildren()) {
603                    locationsToRead.add(new LocationWithDepth(child, read.depth + 1));
604                }
605            }
606            setCacheableInfo(request);
607        }
608    
609        /**
610         * Process a request to read the properties of a node at the supplied location.
611         * <p>
612         * This method does nothing if the request is null.
613         * </p>
614         * 
615         * @param request the read request
616         */
617        public abstract void process( ReadAllPropertiesRequest request );
618    
619        /**
620         * Process a request to read the properties and children of a node at the supplied location.
621         * <p>
622         * This method does nothing if the request is null. Unless overridden, this method converts the single request into a
623         * {@link ReadAllChildrenRequest} and a {@link ReadAllPropertiesRequest}.
624         * </p>
625         * 
626         * @param request the read request
627         */
628        public void process( ReadNodeRequest request ) {
629            if (request == null) return;
630            // Read the properties ...
631            ReadAllPropertiesRequest readProperties = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
632            process(readProperties);
633            if (readProperties.hasError()) {
634                request.setError(readProperties.getError());
635                return;
636            }
637            // Set the actual location ...
638            request.setActualLocationOfNode(readProperties.getActualLocationOfNode());
639    
640            // Read the children ...
641            ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at(), request.inWorkspace());
642            process(readChildren);
643            if (readChildren.hasError()) {
644                request.setError(readChildren.getError());
645                return;
646            }
647            if (request.isCancelled()) return;
648            // Now, copy all of the results into the submitted request ...
649            for (Property property : readProperties) {
650                request.addProperty(property);
651            }
652            for (Location child : readChildren) {
653                request.addChild(child);
654            }
655            setCacheableInfo(request);
656        }
657    
658        /**
659         * Process a request to read a single property of a node at the supplied location.
660         * <p>
661         * This method does nothing if the request is null. Unless overridden, this method converts the request that
662         * {@link ReadAllPropertiesRequest reads the node} and simply returns the one property.
663         * </p>
664         * 
665         * @param request the read request
666         */
667        public void process( ReadPropertyRequest request ) {
668            if (request == null) return;
669            ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.on(), request.inWorkspace());
670            process(readNode);
671            if (readNode.hasError()) {
672                request.setError(readNode.getError());
673                return;
674            }
675            Property property = readNode.getPropertiesByName().get(request.named());
676            request.setProperty(property);
677            // Set the actual location ...
678            request.setActualLocationOfNode(readNode.getActualLocationOfNode());
679            setCacheableInfo(request);
680        }
681    
682        /**
683         * Process a request to verify that a node exists at the supplied location.
684         * <p>
685         * This method does nothing if the request is null. Unless overridden, this method converts the request that
686         * {@link ReadAllPropertiesRequest reads the node} and uses the result to determine if the node exists.
687         * </p>
688         * 
689         * @param request the read request
690         */
691        public void process( VerifyNodeExistsRequest request ) {
692            if (request == null) return;
693            ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.at(), request.inWorkspace());
694            process(readNode);
695            if (readNode.hasError()) {
696                request.setError(readNode.getError());
697                return;
698            }
699            // Set the actual location ...
700            request.setActualLocationOfNode(readNode.getActualLocationOfNode());
701            setCacheableInfo(request);
702        }
703    
704        /**
705         * Process a request to remove the specified property from a node.
706         * <p>
707         * This method does nothing if the request is null. Unless overridden, this method converts this request into a
708         * {@link UpdatePropertiesRequest}.
709         * </p>
710         * 
711         * @param request the request to remove the property
712         */
713        public void process( RemovePropertyRequest request ) {
714            if (request == null) return;
715            Map<Name, Property> properties = Collections.singletonMap(request.propertyName(), null);
716            UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.from(), request.inWorkspace(), properties);
717            process(update);
718            if (update.hasError()) {
719                request.setError(update.getError());
720            }
721            // Set the actual location ...
722            request.setActualLocationOfNode(update.getActualLocationOfNode());
723        }
724    
725        /**
726         * Process a request to set the specified property on a node.
727         * <p>
728         * This method does nothing if the request is null. Unless overridden, this method converts this request into a
729         * {@link UpdatePropertiesRequest}.
730         * </p>
731         * 
732         * @param request the request to set the property
733         */
734        public void process( SetPropertyRequest request ) {
735            if (request == null) return;
736            Property property = request.property();
737            Map<Name, Property> properties = Collections.singletonMap(property.getName(), property);
738            UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.on(), request.inWorkspace(), properties);
739            process(update);
740            if (update.hasError()) {
741                request.setError(update.getError());
742            } else {
743                // Set the actual location ...
744                request.setActualLocationOfNode(update.getActualLocationOfNode());
745            }
746        }
747    
748        /**
749         * Process a request to remove the specified properties from a node.
750         * <p>
751         * This method does nothing if the request is null.
752         * </p>
753         * 
754         * @param request the remove request
755         */
756        public abstract void process( UpdatePropertiesRequest request );
757    
758        /**
759         * Process a request to rename a node specified location into a different location.
760         * <p>
761         * This method does nothing if the request is null. Unless overridden, this method converts the rename into a
762         * {@link MoveBranchRequest move}. However, this only works if the <code>request</code> has a {@link Location#hasPath() path}
763         * for its {@link RenameNodeRequest#at() location}. (If not, this method throws an {@link UnsupportedOperationException} and
764         * must be overridden.)
765         * </p>
766         * 
767         * @param request the rename request
768         */
769        public void process( RenameNodeRequest request ) {
770            if (request == null) return;
771            Location from = request.at();
772            if (!from.hasPath()) {
773                throw new UnsupportedOperationException();
774            }
775            Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(from.getPath(), request.toName());
776            Location to = Location.create(newPath);
777            MoveBranchRequest move = new MoveBranchRequest(from, to, request.inWorkspace());
778            process(move);
779            // Set the actual locations ...
780            request.setActualLocations(move.getActualLocationBefore(), move.getActualLocationAfter());
781        }
782    
783        /**
784         * Close this processor, allowing it to clean up any open resources.
785         */
786        public void close() {
787            // Publish any changes ...
788            if (observer != null && !this.changes.isEmpty()) {
789                String userName = context.getSecurityContext() != null ? context.getSecurityContext().getUserName() : null; 
790                Changes changes = new Changes(userName, getSourceName(), getNowInUtc(), this.changes);
791                observer.notify(changes);
792            }
793        }
794    
795        /**
796         * A class that represents a location at a known depth
797         * 
798         * @author Randall Hauch
799         */
800        @Immutable
801        protected static class LocationWithDepth {
802            protected final Location location;
803            protected final int depth;
804    
805            protected LocationWithDepth( Location location,
806                                         int depth ) {
807                this.location = location;
808                this.depth = depth;
809            }
810    
811            @Override
812            public int hashCode() {
813                return location.hashCode();
814            }
815    
816            @Override
817            public String toString() {
818                return location.toString() + " at depth " + depth;
819            }
820        }
821    
822    }