001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.graph.requests.processor;
023    
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.LinkedList;
027    import java.util.List;
028    import java.util.Queue;
029    import net.jcip.annotations.Immutable;
030    import org.jboss.dna.common.util.CheckArg;
031    import org.jboss.dna.graph.ExecutionContext;
032    import org.jboss.dna.graph.GraphI18n;
033    import org.jboss.dna.graph.Location;
034    import org.jboss.dna.graph.connectors.RepositorySourceException;
035    import org.jboss.dna.graph.properties.DateTime;
036    import org.jboss.dna.graph.properties.Name;
037    import org.jboss.dna.graph.properties.Path;
038    import org.jboss.dna.graph.properties.Property;
039    import org.jboss.dna.graph.properties.basic.BasicEmptyProperty;
040    import org.jboss.dna.graph.requests.CompositeRequest;
041    import org.jboss.dna.graph.requests.CopyBranchRequest;
042    import org.jboss.dna.graph.requests.CreateNodeRequest;
043    import org.jboss.dna.graph.requests.DeleteBranchRequest;
044    import org.jboss.dna.graph.requests.MoveBranchRequest;
045    import org.jboss.dna.graph.requests.ReadAllChildrenRequest;
046    import org.jboss.dna.graph.requests.ReadAllPropertiesRequest;
047    import org.jboss.dna.graph.requests.ReadBlockOfChildrenRequest;
048    import org.jboss.dna.graph.requests.ReadBranchRequest;
049    import org.jboss.dna.graph.requests.ReadNextBlockOfChildrenRequest;
050    import org.jboss.dna.graph.requests.ReadNodeRequest;
051    import org.jboss.dna.graph.requests.ReadPropertyRequest;
052    import org.jboss.dna.graph.requests.RemovePropertiesRequest;
053    import org.jboss.dna.graph.requests.RenameNodeRequest;
054    import org.jboss.dna.graph.requests.Request;
055    import org.jboss.dna.graph.requests.UpdatePropertiesRequest;
056    
057    /**
058     * A component that is used to process and execute {@link Request}s. This class is intended to be subclassed and methods
059     * overwritten to define the behavior for executing the different kinds of requests. Abstract methods must be overridden, but
060     * non-abstract methods all have meaningful default implementations.
061     * 
062     * @author Randall Hauch
063     */
064    @Immutable
065    public abstract class RequestProcessor {
066    
067        private final ExecutionContext context;
068        private final String sourceName;
069        private final DateTime nowInUtc;
070    
071        protected RequestProcessor( String sourceName,
072                                    ExecutionContext context ) {
073            this(sourceName, context, null);
074        }
075    
076        protected RequestProcessor( String sourceName,
077                                    ExecutionContext context,
078                                    DateTime now ) {
079            CheckArg.isNotEmpty(sourceName, "sourceName");
080            CheckArg.isNotNull(context, "context");
081            this.context = context;
082            this.sourceName = sourceName;
083            this.nowInUtc = now != null ? now : context.getValueFactories().getDateFactory().createUtc();
084        }
085    
086        /**
087         * Get the name of the source against which this processor is executing.
088         * 
089         * @return the repository source name; never null or empty
090         */
091        public String getSourceName() {
092            return sourceName;
093        }
094    
095        /**
096         * The execution context that this process is operating within.
097         * 
098         * @return the execution context; never null
099         */
100        public ExecutionContext getExecutionContext() {
101            return this.context;
102        }
103    
104        /**
105         * Get the 'current time' for this processor, which is usually a constant during its lifetime.
106         * 
107         * @return the current time in UTC; never null
108         */
109        protected DateTime getNowInUtc() {
110            return this.nowInUtc;
111        }
112    
113        /**
114         * Process a request by determining the type of request and delegating to the appropriate <code>process</code> method for that
115         * type.
116         * <p>
117         * This method does nothing if the request is null.
118         * </p>
119         * 
120         * @param request the general request
121         */
122        public void process( Request request ) {
123            if (request == null) return;
124            if (request.isCancelled()) return;
125            if (request instanceof CompositeRequest) {
126                process((CompositeRequest)request);
127            } else if (request instanceof CopyBranchRequest) {
128                process((CopyBranchRequest)request);
129            } else if (request instanceof CreateNodeRequest) {
130                process((CreateNodeRequest)request);
131            } else if (request instanceof DeleteBranchRequest) {
132                process((DeleteBranchRequest)request);
133            } else if (request instanceof MoveBranchRequest) {
134                process((MoveBranchRequest)request);
135            } else if (request instanceof ReadAllChildrenRequest) {
136                process((ReadAllChildrenRequest)request);
137            } else if (request instanceof ReadNextBlockOfChildrenRequest) {
138                process((ReadNextBlockOfChildrenRequest)request);
139            } else if (request instanceof ReadBlockOfChildrenRequest) {
140                process((ReadBlockOfChildrenRequest)request);
141            } else if (request instanceof ReadBranchRequest) {
142                process((ReadBranchRequest)request);
143            } else if (request instanceof ReadNodeRequest) {
144                process((ReadNodeRequest)request);
145            } else if (request instanceof ReadAllPropertiesRequest) {
146                process((ReadAllPropertiesRequest)request);
147            } else if (request instanceof ReadPropertyRequest) {
148                process((ReadPropertyRequest)request);
149            } else if (request instanceof RemovePropertiesRequest) {
150                process((RemovePropertiesRequest)request);
151            } else if (request instanceof RenameNodeRequest) {
152                process((RenameNodeRequest)request);
153            } else if (request instanceof UpdatePropertiesRequest) {
154                process((UpdatePropertiesRequest)request);
155            }
156        }
157    
158        /**
159         * Process a request that is composed of multiple other (non-composite) requests. If any of the embedded requests
160         * {@link Request#hasError() has an error} after it is processed, the submitted request will be marked with an error.
161         * <p>
162         * This method does nothing if the request is null.
163         * </p>
164         * 
165         * @param request the composite request
166         */
167        public void process( CompositeRequest request ) {
168            if (request == null) return;
169            int numberOfErrors = 0;
170            Throwable firstError = null;
171            for (Request embedded : request) {
172                assert embedded != null;
173                if (embedded.isCancelled()) return;
174                process(embedded);
175                if (embedded.hasError()) {
176                    if (numberOfErrors == 0) firstError = embedded.getError();
177                    ++numberOfErrors;
178                }
179            }
180            if (firstError == null) return;
181            if (numberOfErrors == 1) {
182                request.setError(firstError);
183            } else {
184                String msg = GraphI18n.multipleErrorsWhileExecutingRequests.text(numberOfErrors, request.size());
185                request.setError(new RepositorySourceException(getSourceName(), msg));
186            }
187        }
188    
189        /**
190         * Process a request to copy a branch into another location.
191         * <p>
192         * This method does nothing if the request is null.
193         * </p>
194         * 
195         * @param request the copy request
196         */
197        public abstract void process( CopyBranchRequest request );
198    
199        /**
200         * Process a request to create a node at a specified location.
201         * <p>
202         * This method does nothing if the request is null.
203         * </p>
204         * 
205         * @param request the create request
206         */
207        public abstract void process( CreateNodeRequest request );
208    
209        /**
210         * Process a request to delete a branch at a specified location.
211         * <p>
212         * This method does nothing if the request is null.
213         * </p>
214         * 
215         * @param request the delete request
216         */
217        public abstract void process( DeleteBranchRequest request );
218    
219        /**
220         * Process a request to move a branch at a specified location into a different location.
221         * <p>
222         * This method does nothing if the request is null.
223         * </p>
224         * 
225         * @param request the move request
226         */
227        public abstract void process( MoveBranchRequest request );
228    
229        /**
230         * Process a request to read all of the children of a node.
231         * <p>
232         * This method does nothing if the request is null.
233         * </p>
234         * 
235         * @param request the read request
236         */
237        public abstract void process( ReadAllChildrenRequest request );
238    
239        /**
240         * Process a request to read a block of the children of a node. The block is defined by a
241         * {@link ReadBlockOfChildrenRequest#startingAtIndex() starting index} and a {@link ReadBlockOfChildrenRequest#count() maximum
242         * number of children to include in the block}.
243         * <p>
244         * This method does nothing if the request is null. The default implementation converts the command to a
245         * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
246         * implementation may not be efficient and may need to be overridden.
247         * </p>
248         * 
249         * @param request the read request
250         */
251        public void process( ReadBlockOfChildrenRequest request ) {
252            if (request == null) return;
253            // Convert the request to a ReadAllChildrenRequest and execute it ...
254            ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(request.of());
255            process(readAll);
256            if (readAll.hasError()) {
257                request.setError(readAll.getError());
258                return;
259            }
260            List<Location> allChildren = readAll.getChildren();
261    
262            // If there aren't enough children for the block's range ...
263            if (allChildren.size() < request.startingAtIndex()) return;
264    
265            // Now, find the children in the block ...
266            int endIndex = Math.min(request.endingBefore(), allChildren.size());
267            for (int i = request.startingAtIndex(); i != endIndex; ++i) {
268                request.addChild(allChildren.get(i));
269            }
270            // Set the actual location ...
271            request.setActualLocationOfNode(readAll.getActualLocationOfNode());
272        }
273    
274        /**
275         * Process a request to read the next block of the children of a node, starting after a previously-retrieved child.
276         * <p>
277         * This method does nothing if the request is null. The default implementation converts the command to a
278         * {@link ReadAllChildrenRequest}, and then finds the children within the block. Obviously for large numbers of children, this
279         * implementation may not be efficient and may need to be overridden.
280         * </p>
281         * 
282         * @param request the read request
283         */
284        public void process( ReadNextBlockOfChildrenRequest request ) {
285            if (request == null) return;
286            // Convert the request to a ReadAllChildrenRequest and execute it ...
287            ReadAllChildrenRequest readAll = new ReadAllChildrenRequest(request.of());
288            process(readAll);
289            if (readAll.hasError()) {
290                request.setError(readAll.getError());
291                return;
292            }
293            List<Location> allChildren = readAll.getChildren();
294    
295            // Iterate through the children, looking for the 'startingAfter' child ...
296            boolean found = false;
297            int count = 0;
298            for (Location child : allChildren) {
299                if (count > request.count()) break;
300                if (!found) {
301                    // Set to true if we find the child we're looking for ...
302                    found = child.equals(request.startingAfter());
303                } else {
304                    // Add the child to the block ...
305                    ++count;
306                    request.addChild(child);
307                }
308            }
309    
310            // Set the actual location ...
311            request.setActualLocationOfNode(readAll.getActualLocationOfNode());
312        }
313    
314        /**
315         * Process a request to read a branch or subgraph that's below a node at a specified location.
316         * <p>
317         * This method does nothing if the request is null. The default implementation processes the branch by submitting the
318         * equivalent requests to {@link ReadNodeRequest read the nodes} and the {@link ReadAllChildrenRequest children}. It starts by
319         * doing this for the top-level node, then proceeds for each of the children of that node, and so forth.
320         * </p>
321         * 
322         * @param request the request to read the branch
323         */
324        public void process( ReadBranchRequest request ) {
325            if (request == null) return;
326            // Create a queue for locations that need to be read ...
327            Queue<LocationWithDepth> locationsToRead = new LinkedList<LocationWithDepth>();
328            locationsToRead.add(new LocationWithDepth(request.at(), 1));
329    
330            // Now read the locations ...
331            boolean first = true;
332            while (locationsToRead.peek() != null) {
333                if (request.isCancelled()) return;
334                LocationWithDepth read = locationsToRead.poll();
335    
336                // Check the depth ...
337                if (read.depth > request.maximumDepth()) break;
338    
339                // Read the properties ...
340                ReadNodeRequest readNode = new ReadNodeRequest(read.location);
341                process(readNode);
342                if (readNode.hasError()) {
343                    request.setError(readNode.getError());
344                    return;
345                }
346                Location actualLocation = readNode.getActualLocationOfNode();
347                if (first) {
348                    // Set the actual location on the original request
349                    request.setActualLocationOfNode(actualLocation);
350                    first = false;
351                }
352    
353                // Record in the request the children and properties that were read on this node ...
354                request.setChildren(actualLocation, readNode.getChildren());
355                request.setProperties(actualLocation, readNode.getProperties());
356    
357                // Add each of the children to the list of locations that we need to read ...
358                for (Location child : readNode.getChildren()) {
359                    locationsToRead.add(new LocationWithDepth(child, read.depth + 1));
360                }
361            }
362        }
363    
364        /**
365         * Process a request to read the properties of a node at the supplied location.
366         * <p>
367         * This method does nothing if the request is null.
368         * </p>
369         * 
370         * @param request the read request
371         */
372        public abstract void process( ReadAllPropertiesRequest request );
373    
374        /**
375         * Process a request to read the properties and children of a node at the supplied location.
376         * <p>
377         * This method does nothing if the request is null. Unless overridden, this method converts the single request into a
378         * {@link ReadAllChildrenRequest} and a {@link ReadAllPropertiesRequest}.
379         * </p>
380         * 
381         * @param request the read request
382         */
383        public void process( ReadNodeRequest request ) {
384            if (request == null) return;
385            // Read the properties ...
386            ReadAllPropertiesRequest readProperties = new ReadAllPropertiesRequest(request.at());
387            process(readProperties);
388            if (readProperties.hasError()) {
389                request.setError(readProperties.getError());
390                return;
391            }
392            // Set the actual location ...
393            request.setActualLocationOfNode(readProperties.getActualLocationOfNode());
394    
395            // Read the children ...
396            ReadAllChildrenRequest readChildren = new ReadAllChildrenRequest(request.at());
397            process(readChildren);
398            if (readChildren.hasError()) {
399                request.setError(readChildren.getError());
400                return;
401            }
402            if (request.isCancelled()) return;
403            // Now, copy all of the results into the submitted request ...
404            for (Property property : readProperties) {
405                request.addProperty(property);
406            }
407            for (Location child : readChildren) {
408                request.addChild(child);
409            }
410        }
411    
412        /**
413         * Process a request to read a single property of a node at the supplied location.
414         * <p>
415         * This method does nothing if the request is null. Unless overridden, this method converts the request that
416         * {@link ReadNodeRequest reads the node} and simply returns the one property.
417         * </p>
418         * 
419         * @param request the read request
420         */
421        public void process( ReadPropertyRequest request ) {
422            if (request == null) return;
423            ReadAllPropertiesRequest readNode = new ReadAllPropertiesRequest(request.on());
424            process(readNode);
425            if (readNode.hasError()) {
426                request.setError(readNode.getError());
427                return;
428            }
429            Property property = readNode.getPropertiesByName().get(request.named());
430            request.setProperty(property);
431            // Set the actual location ...
432            request.setActualLocationOfNode(readNode.getActualLocationOfNode());
433        }
434    
435        /**
436         * Process a request to remove the specified properties from a node.
437         * <p>
438         * This method does nothing if the request is null. Unless overridden, this method converts this request into a
439         * {@link UpdatePropertiesRequest}.
440         * </p>
441         * 
442         * @param request the request to remove the properties with certain names
443         */
444        public void process( RemovePropertiesRequest request ) {
445            if (request == null) return;
446            Collection<Name> names = request.propertyNames();
447            if (names.isEmpty()) return;
448            List<Property> emptyProperties = new ArrayList<Property>(names.size());
449            for (Name propertyName : names) {
450                emptyProperties.add(new BasicEmptyProperty(propertyName));
451            }
452            UpdatePropertiesRequest update = new UpdatePropertiesRequest(request.from(), emptyProperties);
453            process(update);
454            if (update.hasError()) {
455                request.setError(update.getError());
456            }
457            // Set the actual location ...
458            request.setActualLocationOfNode(update.getActualLocationOfNode());
459        }
460    
461        /**
462         * Process a request to remove the specified properties from a node.
463         * <p>
464         * This method does nothing if the request is null.
465         * </p>
466         * 
467         * @param request the remove request
468         */
469        public abstract void process( UpdatePropertiesRequest request );
470    
471        /**
472         * Process a request to rename a node specified location into a different location.
473         * <p>
474         * This method does nothing if the request is null. Unless overridden, this method converts the rename into a
475         * {@link MoveBranchRequest move}. However, this only works if the <code>request</code> has a {@link Location#hasPath() path}
476         * for its {@link RenameNodeRequest#at() location}. (If not, this method throws an {@link UnsupportedOperationException} and
477         * must be overriddent.)
478         * </p>
479         * 
480         * @param request the rename request
481         */
482        public void process( RenameNodeRequest request ) {
483            if (request == null) return;
484            Location from = request.at();
485            if (!from.hasPath()) {
486                throw new UnsupportedOperationException();
487            }
488            Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(from.getPath(), request.toName());
489            Location to = new Location(newPath);
490            MoveBranchRequest move = new MoveBranchRequest(from, to);
491            process(move);
492            // Set the actual locations ...
493            request.setActualLocations(move.getActualLocationBefore(), move.getActualLocationAfter());
494        }
495    
496        /**
497         * Close this processor, allowing it to clean up any open resources.
498         */
499        public void close() {
500            // do nothing
501        }
502    
503        /**
504         * A class that represents a location at a known depth
505         * 
506         * @author Randall Hauch
507         */
508        @Immutable
509        protected static class LocationWithDepth {
510            protected final Location location;
511            protected final int depth;
512    
513            protected LocationWithDepth( Location location,
514                                         int depth ) {
515                this.location = location;
516                this.depth = depth;
517            }
518    
519            @Override
520            public int hashCode() {
521                return location.hashCode();
522            }
523    
524            @Override
525            public String toString() {
526                return location.toString() + " at depth " + depth;
527            }
528        }
529    
530    }