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.session;
25  
26  import java.security.AccessControlException;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.UUID;
39  import net.jcip.annotations.Immutable;
40  import net.jcip.annotations.NotThreadSafe;
41  import net.jcip.annotations.ThreadSafe;
42  import org.modeshape.common.collection.ReadOnlyIterator;
43  import org.modeshape.common.i18n.I18n;
44  import org.modeshape.common.util.CheckArg;
45  import org.modeshape.common.util.StringUtil;
46  import org.modeshape.graph.ExecutionContext;
47  import org.modeshape.graph.Graph;
48  import org.modeshape.graph.GraphI18n;
49  import org.modeshape.graph.JcrLexicon;
50  import org.modeshape.graph.Location;
51  import org.modeshape.graph.Results;
52  import org.modeshape.graph.Subgraph;
53  import org.modeshape.graph.connector.RepositorySourceException;
54  import org.modeshape.graph.connector.UuidAlreadyExistsException;
55  import org.modeshape.graph.property.DateTime;
56  import org.modeshape.graph.property.Name;
57  import org.modeshape.graph.property.NamespaceRegistry;
58  import org.modeshape.graph.property.Path;
59  import org.modeshape.graph.property.PathFactory;
60  import org.modeshape.graph.property.PathNotFoundException;
61  import org.modeshape.graph.property.Property;
62  import org.modeshape.graph.property.Path.Segment;
63  import org.modeshape.graph.request.BatchRequestBuilder;
64  import org.modeshape.graph.request.ChangeRequest;
65  import org.modeshape.graph.request.CloneBranchRequest;
66  import org.modeshape.graph.request.CopyBranchRequest;
67  import org.modeshape.graph.request.InvalidWorkspaceException;
68  import org.modeshape.graph.request.MoveBranchRequest;
69  import org.modeshape.graph.request.Request;
70  import org.modeshape.graph.request.RequestException;
71  import org.modeshape.graph.session.GraphSession.Authorizer.Action;
72  import com.google.common.collect.LinkedListMultimap;
73  import com.google.common.collect.ListMultimap;
74  
75  /**
76   * This class represents an interactive session for working with the content within a graph. This session maintains a cache of
77   * content read from the repository, as well as transient changes that have been made to the nodes within this session that are
78   * then pushed to the graph when the session is {@link #save() saved}.
79   * <p>
80   * Like the other Graph APIs, the mutable objects in this session should not be held onto for very long periods of time. When the
81   * session is {@link #save() saved} or {{@link #refresh(boolean) refreshed} (or when a node is {@link #save(Node) saved} or
82   * {@link #refresh(Node, boolean) refreshed}), the session may {@link Node#unload() unload} and discard some of its nodes. Using
83   * nodes after they are discarded may result in assertion errors (assuming Java assertions are enabled).
84   * </p>
85   * 
86   * @param <Payload> the type of the payload object for each node, used to allow the nodes to hold additional cached information
87   * @param <PropertyPayload> the type of payload object for each property, used to allow the nodes to hold additional cached
88   *        information
89   */
90  @NotThreadSafe
91  public class GraphSession<Payload, PropertyPayload> {
92  
93      protected final ListMultimap<Name, Node<Payload, PropertyPayload>> NO_CHILDREN = LinkedListMultimap.create();
94      protected final Map<Name, PropertyInfo<PropertyPayload>> NO_PROPERTIES = Collections.emptyMap();
95  
96      protected final Authorizer authorizer;
97      protected final ExecutionContext context;
98      protected final Graph store;
99      protected final Node<Payload, PropertyPayload> root;
100     protected final Operations<Payload, PropertyPayload> nodeOperations;
101     protected final PathFactory pathFactory;
102     protected final NodeIdFactory idFactory;
103     protected final String workspaceName;
104     protected int loadDepth = 1;
105 
106     /**
107      * A map of the nodes keyed by their identifier.
108      */
109     protected final Map<NodeId, Node<Payload, PropertyPayload>> nodes = new HashMap<NodeId, Node<Payload, PropertyPayload>>();
110     /**
111      * A map that records how the changes to a node are dependent upon other nodes.
112      */
113     protected final Map<NodeId, Dependencies> changeDependencies = new HashMap<NodeId, Dependencies>();
114 
115     private LinkedList<Request> requests;
116     private BatchRequestBuilder requestBuilder;
117     protected Graph.Batch operations;
118 
119     /**
120      * Create a session that uses the supplied graph and the supplied node operations.
121      * 
122      * @param graph the graph that this session is to use
123      * @param workspaceName the name of the workspace that is to be used, or null if the current workspace should be used
124      * @param nodeOperations the operations that are to be performed during various stages in the lifecycle of a node, or null if
125      *        there are no special operations that should be performed
126      */
127     public GraphSession( Graph graph,
128                          String workspaceName,
129                          Operations<Payload, PropertyPayload> nodeOperations ) {
130         this(graph, workspaceName, nodeOperations, null);
131     }
132 
133     /**
134      * Create a session that uses the supplied graph and the supplied node operations.
135      * 
136      * @param graph the graph that this session is to use
137      * @param workspaceName the name of the workspace that is to be used, or null if the current workspace should be used
138      * @param nodeOperations the operations that are to be performed during various stages in the lifecycle of a node, or null if
139      *        there are no special operations that should be performed
140      * @param authorizer the authorizing component, or null if no special authorization is to be performed
141      * @throws IllegalArgumentException if the graph reference is null
142      * @throws IllegalArgumentException if the depth is not positive
143      */
144     public GraphSession( Graph graph,
145                          String workspaceName,
146                          Operations<Payload, PropertyPayload> nodeOperations,
147                          Authorizer authorizer ) {
148         assert graph != null;
149         this.store = graph;
150         this.context = store.getContext();
151         if (workspaceName != null) {
152             this.workspaceName = this.store.useWorkspace(workspaceName).getName();
153         } else {
154             this.workspaceName = this.store.getCurrentWorkspaceName();
155         }
156         this.nodeOperations = nodeOperations != null ? nodeOperations : new NodeOperations<Payload, PropertyPayload>();
157         this.pathFactory = context.getValueFactories().getPathFactory();
158         this.authorizer = authorizer != null ? authorizer : new NoOpAuthorizer();
159         // Create the NodeId factory ...
160         this.idFactory = new NodeIdFactory() {
161             private long nextId = 0L;
162 
163             public NodeId create() {
164                 return new NodeId(++nextId);
165             }
166         };
167         // Create the root node ...
168         Location rootLocation = Location.create(pathFactory.createRootPath());
169         NodeId rootId = idFactory.create();
170         this.root = createNode(null, rootId, rootLocation);
171         this.nodes.put(rootId, root);
172 
173         // Create the batch operations ...
174         this.requests = new LinkedList<Request>();
175         this.requestBuilder = new BatchRequestBuilder(this.requests);
176         this.operations = this.store.batch(this.requestBuilder);
177     }
178 
179     ExecutionContext context() {
180         return context;
181     }
182 
183     final String readable( Name name ) {
184         return name.getString(context.getNamespaceRegistry());
185     }
186 
187     final String readable( Path.Segment segment ) {
188         return segment.getString(context.getNamespaceRegistry());
189     }
190 
191     final String readable( Path path ) {
192         return path.getString(context.getNamespaceRegistry());
193     }
194 
195     final String readable( Location location ) {
196         return location.getString(context.getNamespaceRegistry());
197     }
198 
199     /**
200      * Get the subgraph depth that is read when a node is loaded from the persistence store. By default, this value is 1.
201      * 
202      * @return the loading depth; always positive
203      */
204     public int getDepthForLoadingNodes() {
205         return loadDepth;
206     }
207 
208     /**
209      * Set the loading depth parameter, which controls how deep a subgraph should be read when a node is loaded from the
210      * persistence store. By default, this value is 1.
211      * 
212      * @param depth the depth that should be read whenever a single node is loaded
213      * @throws IllegalArgumentException if the depth is not positive
214      */
215     public void setDepthForLoadingNodes( int depth ) {
216         CheckArg.isPositive(depth, "depth");
217         this.loadDepth = depth;
218     }
219 
220     /**
221      * Get the root node.
222      * 
223      * @return the root node; never null
224      */
225     public Node<Payload, PropertyPayload> getRoot() {
226         return root;
227     }
228 
229     /**
230      * Get the path factory that should be used to adjust the path objects.
231      * 
232      * @return the path factory; never null
233      */
234     public PathFactory getPathFactory() {
235         return pathFactory;
236     }
237 
238     /**
239      * Find in the session the node with the supplied location. If the location does not have a path, this method must first query
240      * the actual persistent store, even if the session already has loaded the node. Thus, this method may not be the most
241      * efficient technique to find a node.
242      * 
243      * @param location the location of the node
244      * @return the cached node at the supplied location
245      * @throws PathNotFoundException if the node at the supplied location does not exist
246      * @throws AccessControlException if the user does not have permission to read the node given by the supplied location
247      * @throws IllegalArgumentException if the location is null
248      */
249     public Node<Payload, PropertyPayload> findNodeWith( Location location ) throws PathNotFoundException, AccessControlException {
250         if (!location.hasPath()) {
251             UUID uuid = location.getUuid();
252             if (uuid != null) {
253 
254                 // Try to find the node in the cache
255                 for (Node<Payload, PropertyPayload> node : nodes.values()) {
256                     UUID nodeUuid = uuidFor(node.getLocation());
257 
258                     if (uuid.equals(nodeUuid)) {
259                         return node;
260                     }
261                 }
262             }
263 
264             // Query for the actual location ...
265             location = store.getNodeAt(location).getLocation();
266         }
267         assert location.hasPath();
268         return findNodeWith(null, location.getPath());
269     }
270 
271     private UUID uuidFor( Location location ) {
272         UUID uuid = location.getUuid();
273         if (uuid != null) return uuid;
274 
275         Property idProp = location.getIdProperty(JcrLexicon.UUID);
276         if (idProp == null) return null;
277 
278         return (UUID)idProp.getFirstValue();
279     }
280 
281     /**
282      * Find in the session the node with the supplied identifier.
283      * 
284      * @param id the identifier of the node
285      * @return the identified node, or null if the session has no node with the supplied identifier
286      * @throws IllegalArgumentException if the identifier is null
287      */
288     public Node<Payload, PropertyPayload> findNodeWith( NodeId id ) {
289         CheckArg.isNotNull(id, "id");
290         return nodes.get(id);
291     }
292 
293     /**
294      * Find the node with the supplied identifier or, if no such node is found, the node at the supplied path. Note that if a node
295      * was found by the identifier, the resulting may not have the same path as that supplied as a parameter.
296      * 
297      * @param id the identifier to the node; may be null if the node is to be found by path
298      * @param path the path that should be used to find the node only when the cache doesn't contain a node with the identifier
299      * @return the node with the supplied id and/or path
300      * @throws IllegalArgumentException if the identifier and path are both node
301      * @throws PathNotFoundException if the node at the supplied path does not exist
302      * @throws AccessControlException if the user does not have permission to read the nodes given by the supplied path
303      */
304     public Node<Payload, PropertyPayload> findNodeWith( NodeId id,
305                                                         Path path ) throws PathNotFoundException, AccessControlException {
306         if (id == null && path == null) {
307             CheckArg.isNotNull(id, "id");
308             CheckArg.isNotNull(path, "path");
309         }
310         Node<Payload, PropertyPayload> result = id != null ? nodes.get(id) : null; // if found, the user should have read
311         // privilege since it was
312         // already in the cache
313         if (result == null || result.isStale()) {
314             assert path != null;
315             result = findNodeWith(path);
316         }
317         return result;
318     }
319 
320     /**
321      * Find the node with the supplied path. This node quickly finds the node if it exists in the cache, or if it is not in the
322      * cache, it loads the nodes down the supplied path.
323      * 
324      * @param path the path to the node
325      * @return the node information
326      * @throws PathNotFoundException if the node at the supplied path does not exist
327      * @throws AccessControlException if the user does not have permission to read the nodes given by the supplied path
328      */
329     public Node<Payload, PropertyPayload> findNodeWith( Path path ) throws PathNotFoundException, AccessControlException {
330         if (path.isRoot()) return getRoot();
331         if (path.isIdentifier()) return findNodeWith(Location.create(path));
332         return findNodeRelativeTo(root, path.relativeTo(root.getPath()), true);
333     }
334 
335     /**
336      * Find the node with the supplied path. This node quickly finds the node if it exists in the cache, or if it is not in the
337      * cache, it loads the nodes down the supplied path. However, if <code>loadIfRequired</code> is <code>false</code>, then any
338      * node along the path that is not loaded will result in this method returning null.
339      * 
340      * @param path the path to the node
341      * @param loadIfRequired true if any missing nodes should be loaded, or false if null should be returned if any nodes along
342      *        the path are not loaded
343      * @return the node information
344      * @throws PathNotFoundException if the node at the supplied path does not exist
345      * @throws AccessControlException if the user does not have permission to read the nodes given by the supplied path
346      */
347     protected Node<Payload, PropertyPayload> findNodeWith( Path path,
348                                                            boolean loadIfRequired )
349         throws PathNotFoundException, AccessControlException {
350         if (path.isRoot()) return getRoot();
351         if (path.isIdentifier()) return findNodeWith(Location.create(path));
352         return findNodeRelativeTo(root, path.relativeTo(root.getPath()), loadIfRequired);
353     }
354 
355     /**
356      * Find the node with the supplied path relative to another node. This node quickly finds the node by walking the supplied
357      * relative path starting at the supplied node. As soon as a cached node is found to not be fully loaded, the persistent
358      * information for that node and all remaining nodes along the relative path are read from the persistent store and inserted
359      * into the cache.
360      * 
361      * @param startingPoint the node from which the path is relative
362      * @param relativePath the relative path from the designated starting point to the desired node; may not be null and may not
363      *        be an {@link Path#isAbsolute() absolute} path
364      * @return the node information
365      * @throws PathNotFoundException if the node at the supplied path does not exist
366      * @throws AccessControlException if the user does not have permission to read the nodes given by the supplied path
367      */
368     public Node<Payload, PropertyPayload> findNodeRelativeTo( Node<Payload, PropertyPayload> startingPoint,
369                                                               Path relativePath )
370         throws PathNotFoundException, AccessControlException {
371         return findNodeRelativeTo(startingPoint, relativePath, true);
372     }
373 
374     /**
375      * Find the node with the supplied path relative to another node. This node quickly finds the node by walking the supplied
376      * relative path starting at the supplied node. As soon as a cached node is found to not be fully loaded, the persistent
377      * information for that node and all remaining nodes along the relative path are read from the persistent store and inserted
378      * into the cache.
379      * 
380      * @param startingPoint the node from which the path is relative
381      * @param relativePath the relative path from the designated starting point to the desired node; may not be null and may not
382      *        be an {@link Path#isAbsolute() absolute} path
383      * @param loadIfRequired true if any missing nodes should be loaded, or false if null should be returned if any nodes along
384      *        the path are not loaded
385      * @return the node information, or null if the node was not yet loaded (and <code>loadRequired</code> was false)
386      * @throws PathNotFoundException if the node at the supplied path does not exist
387      * @throws AccessControlException if the user does not have permission to read the nodes given by the supplied path
388      */
389     @SuppressWarnings( "synthetic-access" )
390     protected Node<Payload, PropertyPayload> findNodeRelativeTo( Node<Payload, PropertyPayload> startingPoint,
391                                                                  Path relativePath,
392                                                                  boolean loadIfRequired )
393         throws PathNotFoundException, AccessControlException {
394         Node<Payload, PropertyPayload> node = startingPoint;
395         if (!relativePath.isRoot()) {
396             // Find the absolute path, which ensures that the relative path is well-formed ...
397             Path absolutePath = relativePath.resolveAgainst(startingPoint.getPath());
398 
399             // Verify that the user has the appropriate privileges to read these nodes...
400             authorizer.checkPermissions(absolutePath, Action.READ);
401 
402             // Walk down the path ...
403             Iterator<Path.Segment> iter = relativePath.iterator();
404             while (iter.hasNext()) {
405                 Path.Segment segment = iter.next();
406                 try {
407                     if (segment.isSelfReference()) continue;
408                     if (segment.isParentReference()) {
409                         node = node.getParent();
410                         assert node != null; // since the relative path is well-formed
411                         continue;
412                     }
413 
414                     if (node.isLoaded()) {
415                         // The child is the next node we need to process ...
416                         node = node.getChild(segment);
417                     } else {
418                         if (!loadIfRequired) return null;
419                         // The node has not yet been loaded into the cache, so read this node
420                         // from the store as well as all nodes along the path to the node we're really
421                         // interested in. We'll do this in a batch, so first create this batch ...
422                         Graph.Batch batch = store.batch();
423 
424                         // Figure out which nodes along the path need to be loaded from the store ...
425                         Path firstPath = node.getPath();
426                         batch.read(firstPath);
427                         // Now add the path to the child (which is no longer on the iterator) ...
428                         Path nextPath = pathFactory.create(firstPath, segment);
429                         if (!iter.hasNext() && loadDepth > 1) {
430                             batch.readSubgraphOfDepth(loadDepth).at(nextPath);
431                         } else {
432                             batch.read(nextPath);
433                         }
434                         // Now add any remaining paths that are still on the iterator ...
435                         while (iter.hasNext()) {
436                             nextPath = pathFactory.create(nextPath, iter.next());
437                             if (!iter.hasNext() && loadDepth > 1) {
438                                 batch.readSubgraphOfDepth(loadDepth).at(nextPath);
439                             } else {
440                                 batch.read(nextPath);
441                             }
442                         }
443 
444                         // Load all of the nodes (we should be reading at least 2 nodes) ...
445                         Results batchResults = batch.execute();
446 
447                         // Add the children and properties in the lowest cached node ...
448                         Path previousPath = null;
449                         Node<Payload, PropertyPayload> topNode = node;
450                         Node<Payload, PropertyPayload> previousNode = node;
451                         for (org.modeshape.graph.Node persistentNode : batchResults) {
452                             Location location = persistentNode.getLocation();
453                             Path path = location.getPath();
454                             if (path.isRoot()) {
455                                 previousNode = root;
456                                 root.location = location;
457                             } else {
458                                 if (path.getParent().equals(previousPath)) {
459                                     previousNode = previousNode.getChild(path.getLastSegment());
460                                 } else {
461                                     Path subgraphPath = path.relativeTo(topNode.getPath());
462                                     previousNode = findNodeRelativeTo(topNode, subgraphPath);
463                                 }
464                                 // Set the node that we're looking for ...
465                                 if (path.getLastSegment().equals(relativePath.getLastSegment()) && path.equals(absolutePath)) {
466                                     node = previousNode;
467                                 }
468                             }
469                             nodeOperations.materialize(persistentNode, previousNode);
470                             previousPath = path;
471                         }
472                     }
473                 } catch (RequestException re) {
474                     // This can happen if there are multiple segments in the relative path and one of
475                     // segment does not exist. Try to resubmit the requests one at a time.
476                     try {
477                         // Walk down the path ...
478                         Iterator<Path.Segment> redoIter = relativePath.iterator();
479                         Node<Payload, PropertyPayload> redoNode = startingPoint;
480                         while (redoIter.hasNext()) {
481                             Path.Segment redoSegment = redoIter.next();
482                             if (redoSegment.isSelfReference()) continue;
483                             if (redoSegment.isParentReference()) {
484                                 redoNode = redoNode.getParent();
485                                 assert redoNode != null; // since the relative path is well-formed
486                                 continue;
487                             }
488                             Path firstPath = redoNode.getPath();
489 
490                             if (redoNode.isLoaded()) {
491                                 // The child is the next node we need to process ...
492                                 redoNode = redoNode.getChild(redoSegment);
493                             } else {
494                                 Path nextPath = firstPath;
495 
496                                 while (redoIter.hasNext()) {
497                                     nextPath = pathFactory.create(nextPath, redoSegment);
498                                     store.getNodeAt(nextPath);
499                                 }
500                             }
501                         }
502                     } catch (PathNotFoundException e) {
503                         // Use the correct desired path ...
504                         throw new PathNotFoundException(Location.create(relativePath), e.getLowestAncestorThatDoesExist());
505                     }
506 
507                 } catch (PathNotFoundException e) {
508                     // Use the correct desired path ...
509                     throw new PathNotFoundException(Location.create(relativePath), e.getLowestAncestorThatDoesExist());
510                 }
511             }
512         }
513         return node;
514     }
515 
516     /**
517      * Returns whether the session cache has any pending changes that need to be executed.
518      * 
519      * @return true if there are pending changes, or false if there is currently no changes
520      */
521     public boolean hasPendingChanges() {
522         return root.isChanged(true);
523     }
524 
525     /**
526      * Remove any cached information that has been marked as a transient change.
527      */
528     public void clearAllChangedNodes() {
529         root.clearChanges();
530         changeDependencies.clear();
531         requests.clear();
532     }
533 
534     /**
535      * Move this node from its current location so that is is a child of the supplied parent, doing so immediately without
536      * enqueuing the operation within the session's operations. The current session is modified immediately to reflect the move
537      * result.
538      * 
539      * @param nodeToMove the path to the node that is to be moved; may not be null
540      * @param destination the desired new path; may not be null
541      * @throws IllegalArgumentException if the node being moved is the root node
542      * @throws AccessControlException if the caller does not have the permission to perform the operation
543      * @throws RepositorySourceException if any error resulting while performing the operation
544      */
545     public void immediateMove( Path nodeToMove,
546                                Path destination ) throws AccessControlException, RepositorySourceException {
547         CheckArg.isNotNull(nodeToMove, "nodeToMove");
548         CheckArg.isNotNull(destination, "destination");
549 
550         Path newParentPath = destination.getParent();
551         Name newName = destination.getLastSegment().getName();
552 
553         // Check authorization ...
554         authorizer.checkPermissions(newParentPath, Action.ADD_NODE);
555         authorizer.checkPermissions(nodeToMove.getParent(), Action.REMOVE);
556 
557         // Perform the move operation, but use a batch so that we can read the latest list of children ...
558         Results results = store.batch().move(nodeToMove).as(newName).into(newParentPath).execute();
559         MoveBranchRequest moveRequest = (MoveBranchRequest)results.getRequests().get(0);
560         Location locationAfter = moveRequest.getActualLocationAfter();
561 
562         // Find the parent node in the session ...
563         Node<Payload, PropertyPayload> parent = this.findNodeWith(locationAfter.getPath().getParent(), false);
564         if (parent != null && parent.isLoaded()) {
565             // Update the children to make them match the latest snapshot from the store ...
566             parent.synchronizeWithNewlyPersistedNode(locationAfter);
567         }
568     }
569 
570     /**
571      * Copy the node at the supplied source path in the named workspace, and place the copy at the supplied location within the
572      * current workspace, doing so immediately without enqueuing the operation within the session's operations. The current
573      * session is modified immediately to reflect the copy result.
574      * <p>
575      * Note that the destination path should not include a same-name-sibling index, since this will be ignored and will always be
576      * recomputed (as the copy will be appended to any children already in the destination's parent).
577      * </p>
578      * 
579      * @param source the path to the node that is to be copied; may not be null
580      * @param destination the path where the copy is to be placed; may not be null
581      * @throws IllegalArgumentException either path is null or invalid
582      * @throws AccessControlException if the caller does not have the permission to perform the operation
583      * @throws RepositorySourceException if any error resulting while performing the operation
584      */
585     public void immediateCopy( Path source,
586                                Path destination ) throws AccessControlException, RepositorySourceException {
587         immediateCopy(source, workspaceName, destination);
588     }
589 
590     /**
591      * Copy the node at the supplied source path in the named workspace, and place the copy at the supplied location within the
592      * current workspace, doing so immediately without enqueuing the operation within the session's operations. The current
593      * session is modified immediately to reflect the copy result.
594      * <p>
595      * Note that the destination path should not include a same-name-sibling index, since this will be ignored and will always be
596      * recomputed (as the copy will be appended to any children already in the destination's parent).
597      * </p>
598      * 
599      * @param source the path to the node that is to be copied; may not be null
600      * @param sourceWorkspace the name of the workspace where the source node is to be found, or null if the current workspace
601      *        should be used
602      * @param destination the path where the copy is to be placed; may not be null
603      * @throws IllegalArgumentException either path is null or invalid
604      * @throws PathNotFoundException if the node being copied or the parent of the destination path do not exist
605      * @throws InvalidWorkspaceException if the source workspace name is invalid or does not exist
606      * @throws AccessControlException if the caller does not have the permission to perform the operation
607      * @throws RepositorySourceException if any error resulting while performing the operation
608      */
609     public void immediateCopy( Path source,
610                                String sourceWorkspace,
611                                Path destination )
612         throws InvalidWorkspaceException, AccessControlException, PathNotFoundException, RepositorySourceException {
613         CheckArg.isNotNull(source, "source");
614         CheckArg.isNotNull(destination, "destination");
615         if (sourceWorkspace == null) sourceWorkspace = workspaceName;
616 
617         // Check authorization ...
618         authorizer.checkPermissions(destination, Action.ADD_NODE);
619         authorizer.checkPermissions(source, Action.READ);
620 
621         // Perform the copy operation, but use the "to" form (not the "into", which takes the parent), but
622         // but use a batch so that we can read the latest list of children ...
623         Results results = store.batch().copy(source).fromWorkspace(sourceWorkspace).to(destination).execute();
624 
625         // Find the copy request to get the actual location of the copy ...
626         CopyBranchRequest request = (CopyBranchRequest)results.getRequests().get(0);
627         Location locationOfCopy = request.getActualLocationAfter();
628 
629         // Find the parent node in the session ...
630         Node<Payload, PropertyPayload> parent = this.findNodeWith(locationOfCopy.getPath().getParent(), false);
631         if (parent != null && parent.isLoaded()) {
632             // Update the children to make them match the latest snapshot from the store ...
633             parent.synchronizeWithNewlyPersistedNode(locationOfCopy);
634         }
635     }
636 
637     /**
638      * Clone the supplied source branch and place into the destination location, optionally removing any existing copy that
639      * already exists in the destination location, doing so immediately without enqueuing the operation within the session's
640      * operations. The current session is modified immediately to reflect the clone result.
641      * 
642      * @param source the path to the node that is to be cloned; may not be null
643      * @param sourceWorkspace the name of the workspace where the source node is to be found, or null if the current workspace
644      *        should be used
645      * @param destination the path for the new cloned copy; may not be null index
646      * @param removeExisting true if the original should be removed, or false if the original should be left
647      * @param destPathIncludesSegment true if the destination path includes the segment that should be used
648      * @throws IllegalArgumentException either path is null or invalid
649      * @throws InvalidWorkspaceException if the source workspace name is invalid or does not exist
650      * @throws UuidAlreadyExistsException if copy could not be completed because the current workspace already includes at least
651      *         one of the nodes at or below the <code>source</code> branch in the source workspace
652      * @throws PathNotFoundException if the node being clone or the destination node do not exist
653      * @throws AccessControlException if the caller does not have the permission to perform the operation
654      * @throws RepositorySourceException if any error resulting while performing the operation
655      */
656     public void immediateClone( Path source,
657                                 String sourceWorkspace,
658                                 Path destination,
659                                 boolean removeExisting,
660                                 boolean destPathIncludesSegment )
661         throws InvalidWorkspaceException, AccessControlException, UuidAlreadyExistsException, PathNotFoundException,
662         RepositorySourceException {
663         CheckArg.isNotNull(source, "source");
664         CheckArg.isNotNull(destination, "destination");
665         if (sourceWorkspace == null) sourceWorkspace = workspaceName;
666 
667         // Check authorization ...
668         authorizer.checkPermissions(destination.getParent(), Action.ADD_NODE);
669         authorizer.checkPermissions(source, Action.READ);
670 
671         // Perform the copy operation, but use the "to" form (not the "into", which takes the parent), but
672         // but use a batch so that we can read the latest list of children ...
673         Graph.Batch batch = store.batch();
674         if (removeExisting) {
675             // Perform the copy operation, but use the "to" form (not the "into", which takes the parent) ...
676             if (destPathIncludesSegment) {
677                 batch.clone(source)
678                      .fromWorkspace(sourceWorkspace)
679                      .as(destination.getLastSegment())
680                      .into(destination.getParent())
681                      .replacingExistingNodesWithSameUuids();
682             } else {
683                 Name newNodeName = destination.getLastSegment().getName();
684                 batch.clone(source)
685                      .fromWorkspace(sourceWorkspace)
686                      .as(newNodeName)
687                      .into(destination.getParent())
688                      .replacingExistingNodesWithSameUuids();
689             }
690         } else {
691             // Perform the copy operation, but use the "to" form (not the "into", which takes the parent) ...
692             if (destPathIncludesSegment) {
693                 batch.clone(source)
694                      .fromWorkspace(sourceWorkspace)
695                      .as(destination.getLastSegment())
696                      .into(destination.getParent())
697                      .failingIfAnyUuidsMatch();
698             } else {
699                 Name newNodeName = destination.getLastSegment().getName();
700                 batch.clone(source)
701                      .fromWorkspace(sourceWorkspace)
702                      .as(newNodeName)
703                      .into(destination.getParent())
704                      .failingIfAnyUuidsMatch();
705             }
706         }
707         // Now execute these two operations ...
708         Results results = batch.execute();
709 
710         // Find the copy request to get the actual location of the copy ...
711         CloneBranchRequest request = (CloneBranchRequest)results.getRequests().get(0);
712         Location locationOfCopy = request.getActualLocationAfter();
713 
714         // Remove from the session all of the nodes that were removed as part of this clone ...
715         Set<Path> removedAlready = new HashSet<Path>();
716         for (Location removed : request.getRemovedNodes()) {
717             Path path = removed.getPath();
718             if (isBelow(path, removedAlready)) {
719                 // This node is below a node we've already removed, so skip it ...
720                 continue;
721             }
722             Node<Payload, PropertyPayload> removedNode = findNodeWith(path, false);
723             removedNode.remove(false);
724             removedAlready.add(path);
725         }
726 
727         // Find the parent node in the session ...
728         Node<Payload, PropertyPayload> parent = this.findNodeWith(locationOfCopy.getPath().getParent(), false);
729         if (parent != null && parent.isLoaded()) {
730             // Update the children to make them match the latest snapshot from the store ...
731             parent.synchronizeWithNewlyPersistedNode(locationOfCopy);
732         }
733     }
734 
735     private static final boolean isBelow( Path path,
736                                           Collection<Path> paths ) {
737         for (Path aPath : paths) {
738             if (aPath.isAncestorOf(path)) return true;
739         }
740         return false;
741     }
742 
743     /**
744      * Refreshes (removes the cached state) for all cached nodes.
745      * <p>
746      * If {@code keepChanges == true}, modified nodes will not have their state refreshed, while all others will either be
747      * unloaded or changed to reflect the current state of the persistent store.
748      * </p>
749      * 
750      * @param keepChanges indicates whether changed nodes should be kept or refreshed from the repository.
751      * @throws InvalidStateException if any error resulting while reading information from the repository
752      * @throws RepositorySourceException if any error resulting while reading information from the repository
753      */
754     @SuppressWarnings( "synthetic-access" )
755     public void refresh( boolean keepChanges ) throws InvalidStateException, RepositorySourceException {
756         if (keepChanges) {
757             refresh(root, keepChanges);
758         } else {
759             // Clear out all state ...
760             nodes.clear();
761             nodes.put(root.getNodeId(), root);
762             // Clear out all changes ...
763             requests.clear();
764             changeDependencies.clear();
765             // And force the root node to be 'unloaded' (in an efficient way) ...
766             root.status = Status.UNCHANGED;
767             root.childrenByName = null;
768             root.expirationTime = Long.MAX_VALUE;
769             root.changedBelow = false;
770             root.payload = null;
771         }
772     }
773 
774     /**
775      * Refreshes (removes the cached state) for the given node and its descendants.
776      * <p>
777      * If {@code keepChanges == true}, modified nodes will not have their state refreshed, while all others will either be
778      * unloaded or changed to reflect the current state of the persistent store.
779      * </p>
780      * 
781      * @param node the node that is to be refreshed; may not be null
782      * @param keepChanges indicates whether changed nodes should be kept or refreshed from the repository.
783      * @throws InvalidStateException if any error resulting while reading information from the repository
784      * @throws RepositorySourceException if any error resulting while reading information from the repository
785      */
786     public void refresh( Node<Payload, PropertyPayload> node,
787                          boolean keepChanges ) throws InvalidStateException, RepositorySourceException {
788         if (!node.isRoot() && node.isChanged(true)) {
789             // Need to make sure that changes to this branch are not dependent upon changes to nodes outside of this branch...
790             if (node.containsChangesWithExternalDependencies()) {
791                 I18n msg = GraphI18n.unableToRefreshBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
792                 String path = readable(node.getPath());
793                 throw new InvalidStateException(msg.text(path, workspaceName));
794             }
795         }
796 
797         if (keepChanges && node.isChanged(true)) {
798             // Perform the refresh while retaining changes ...
799             // Phase 1: determine which nodes can be unloaded, which must be refreshed, and which must be unchanged ...
800             RefreshState<Payload, PropertyPayload> refreshState = new RefreshState<Payload, PropertyPayload>();
801             node.refreshPhase1(refreshState);
802             // If there are any nodes to be refreshed, read then in a single batch ...
803             Results readResults = null;
804             if (!refreshState.getNodesToBeRefreshed().isEmpty()) {
805                 Graph.Batch batch = store.batch();
806                 for (Node<Payload, PropertyPayload> nodeToBeRefreshed : refreshState.getNodesToBeRefreshed()) {
807                     batch.read(nodeToBeRefreshed.getLocation());
808                 }
809                 // Execute the reads. No modifications have been made to the cache, so it is not a problem
810                 // if this throws a repository exception.
811                 try {
812                     readResults = batch.execute();
813                 } catch (org.modeshape.graph.property.PathNotFoundException e) {
814                     throw new InvalidStateException(e.getLocalizedMessage(), e);
815                 }
816             }
817 
818             // Phase 2: update the cache by unloading or refreshing the nodes ...
819             node.refreshPhase2(refreshState, readResults);
820         } else {
821             // Get rid of all changes ...
822             node.clearChanges();
823             // And then unload the node ...
824             node.unload();
825 
826             // Throw out the old pending operations
827             if (operations.isExecuteRequired()) {
828                 // Make sure the builder has finished all the requests ...
829                 this.requestBuilder.finishPendingRequest();
830 
831                 // Remove all of the enqueued requests for this branch ...
832                 for (Iterator<Request> iter = this.requests.iterator(); iter.hasNext();) {
833                     Request request = iter.next();
834                     assert request instanceof ChangeRequest;
835                     ChangeRequest change = (ChangeRequest)request;
836                     if (change.changes(workspaceName, node.getPath())) {
837                         iter.remove();
838                     }
839                 }
840             }
841         }
842     }
843 
844     /**
845      * Refreshes all properties for the given node only. This refresh always discards changed properties.
846      * <p>
847      * This method is not recursive and will not modify or access any descendants of the given node.
848      * </p>
849      * <p>
850      * <b>NOTE: Calling this method on a node that already has modified properties can result in the enqueued property changes
851      * overwriting the current properties on a save() call. This method should be used with great care to avoid this
852      * situation.</b>
853      * </p>
854      * 
855      * @param node the node for which the properties are to be refreshed; may not be null
856      * @throws InvalidStateException if the node is new
857      * @throws RepositorySourceException if any error resulting while reading information from the repository
858      */
859     public void refreshProperties( Node<Payload, PropertyPayload> node ) throws InvalidStateException, RepositorySourceException {
860         assert node != null;
861 
862         if (node.isNew()) {
863             I18n msg = GraphI18n.unableToRefreshPropertiesBecauseNodeIsModified;
864             String path = readable(node.getPath());
865             throw new InvalidStateException(msg.text(path, workspaceName));
866         }
867 
868         org.modeshape.graph.Node persistentNode = store.getNodeAt(node.getLocation());
869         nodeOperations.materializeProperties(persistentNode, node);
870     }
871 
872     /**
873      * Save any changes that have been accumulated by this session.
874      * 
875      * @throws PathNotFoundException if the state of this session is invalid and is attempting to change a node that doesn't exist
876      * @throws ValidationException if any of the changes being made result in an invalid node state
877      * @throws InvalidStateException if the supplied node is no longer a node within this cache (because it was unloaded)
878      */
879     public void save() throws PathNotFoundException, ValidationException, InvalidStateException {
880         if (!operations.isExecuteRequired()) {
881             // Remove all the cached items ...
882             this.root.clearChanges();
883             this.root.unload();
884             return;
885         }
886 
887         if (!root.isChanged(true)) {
888             // Then a bunch of changes could have been made and rolled back manually, so recompute the change state ...
889             root.recomputeChangedBelow();
890             if (!root.isChanged(true)) {
891                 // If still no changes, then simply do a refresh ...
892                 this.root.clearChanges();
893                 this.root.unload();
894                 return;
895             }
896         }
897 
898         // Make sure that each of the changed node is valid. This process requires that all children of
899         // all changed nodes are loaded, so in this process load all unloaded children in one batch ...
900         final DateTime saveTime = context.getValueFactories().getDateFactory().create();
901         root.onChangedNodes(new LoadAllChildrenVisitor() {
902             @Override
903             protected void finishParentAfterLoading( Node<Payload, PropertyPayload> node ) {
904                 nodeOperations.preSave(node, saveTime);
905             }
906         });
907 
908         root.onChangedNodes(new LoadAllChildrenVisitor() {
909             @Override
910             protected void finishParentAfterLoading( Node<Payload, PropertyPayload> node ) {
911                 nodeOperations.compute(operations, node);
912             }
913         });
914 
915         // Execute the batched operations ...
916         try {
917             operations.execute();
918         } catch (org.modeshape.graph.property.PathNotFoundException e) {
919             throw new InvalidStateException(e.getLocalizedMessage(), e);
920         } catch (RuntimeException e) {
921             throw new RepositorySourceException(e.getLocalizedMessage(), e);
922         }
923 
924         // Create a new batch for future operations ...
925         // LinkedList<Request> oldRequests = this.requests;
926         this.requests = new LinkedList<Request>();
927         this.requestBuilder = new BatchRequestBuilder(this.requests);
928         this.operations = store.batch(this.requestBuilder);
929 
930         // Remove all the cached items ...
931         this.root.clearChanges();
932         this.root.unload();
933     }
934 
935     /**
936      * Save any changes to the identified node or its descendants. The supplied node may not have been deleted or created in this
937      * session since the last save operation.
938      * 
939      * @param node the node being saved; may not be null
940      * @throws PathNotFoundException if the state of this session is invalid and is attempting to change a node that doesn't exist
941      * @throws ValidationException if any of the changes being made result in an invalid node state
942      * @throws InvalidStateException if the supplied node is no longer a node within this cache (because it was unloaded)
943      */
944     public void save( Node<Payload, PropertyPayload> node )
945         throws PathNotFoundException, ValidationException, InvalidStateException {
946         assert node != null;
947         if (node.isRoot()) {
948             // We're actually saving the root, so the other 'save' method is faster and more efficient ...
949             save();
950             return;
951         }
952         if (node.isStale()) {
953             // This node was deleted in this session ...
954             String readableLocation = readable(node.getLocation());
955             I18n msg = GraphI18n.nodeHasAlreadyBeenRemovedFromThisSession;
956             throw new InvalidStateException(msg.text(readableLocation, workspaceName));
957         }
958         if (node.isNew()) {
959             String path = readable(node.getPath());
960             throw new RepositorySourceException(GraphI18n.unableToSaveNodeThatWasCreatedSincePreviousSave.text(path,
961                                                                                                                workspaceName));
962         }
963         if (!node.isChanged(true)) {
964             // There are no changes within this branch
965             return;
966         }
967 
968         // Need to make sure that changes to this branch are not dependent upon changes to nodes outside of this branch...
969         if (node.containsChangesWithExternalDependencies()) {
970             I18n msg = GraphI18n.unableToSaveBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
971             String path = readable(node.getPath());
972             throw new ValidationException(msg.text(path, workspaceName));
973         }
974 
975         // Make sure that each of the changed node is valid. This process requires that all children of
976         // all changed nodes are loaded, so in this process load all unloaded children in one batch ...
977         final DateTime saveTime = context.getValueFactories().getDateFactory().create();
978         root.onChangedNodes(new LoadAllChildrenVisitor() {
979             @Override
980             protected void finishParentAfterLoading( Node<Payload, PropertyPayload> node ) {
981                 nodeOperations.preSave(node, saveTime);
982             }
983         });
984 
985         // Make sure the builder has finished all the requests ...
986         this.requestBuilder.finishPendingRequest();
987 
988         // Remove all of the enqueued requests for this branch ...
989         Path path = node.getPath();
990         LinkedList<Request> branchRequests = new LinkedList<Request>();
991         LinkedList<Request> nonBranchRequests = new LinkedList<Request>();
992         for (Request request : this.requests) {
993             assert request instanceof ChangeRequest;
994             ChangeRequest change = (ChangeRequest)request;
995             if (change.changes(workspaceName, path)) {
996                 branchRequests.add(request);
997             } else {
998                 nonBranchRequests.add(request);
999             }
1000         }
1001         if (branchRequests.isEmpty()) return;
1002 
1003         // Now execute the branch ...
1004         final Graph.Batch branchBatch = store.batch(new BatchRequestBuilder(branchRequests));
1005 
1006         node.onChangedNodes(new LoadAllChildrenVisitor() {
1007             @Override
1008             protected void finishParentAfterLoading( Node<Payload, PropertyPayload> node ) {
1009                 nodeOperations.compute(branchBatch, node);
1010             }
1011         });
1012 
1013         try {
1014             branchBatch.execute();
1015         } catch (org.modeshape.graph.property.PathNotFoundException e) {
1016             throw new InvalidStateException(e.getLocalizedMessage(), e);
1017         } catch (RuntimeException e) {
1018             throw new RepositorySourceException(e.getLocalizedMessage(), e);
1019         }
1020 
1021         // Still have non-branch related requests that we haven't executed ...
1022         this.requests = nonBranchRequests;
1023         this.requestBuilder = new BatchRequestBuilder(this.requests);
1024         this.operations = store.batch(this.requestBuilder);
1025 
1026         // Remove all the cached, changed or deleted items that were just saved ...
1027         node.clearChanges();
1028         node.unload();
1029     }
1030 
1031     protected Node<Payload, PropertyPayload> createNode( Node<Payload, PropertyPayload> parent,
1032                                                          NodeId nodeId,
1033                                                          Location location ) {
1034         return new Node<Payload, PropertyPayload>(this, parent, nodeId, location);
1035     }
1036 
1037     protected long getCurrentTime() {
1038         return System.currentTimeMillis();
1039     }
1040 
1041     protected void recordMove( Node<Payload, PropertyPayload> nodeBeingMoved,
1042                                Node<Payload, PropertyPayload> oldParent,
1043                                Node<Payload, PropertyPayload> newParent ) {
1044         // Fix the cache's state ...
1045         NodeId id = nodeBeingMoved.getNodeId();
1046         Dependencies dependencies = changeDependencies.get(id);
1047         if (dependencies == null) {
1048             dependencies = new Dependencies();
1049             dependencies.setMovedFrom(oldParent.getNodeId());
1050             changeDependencies.put(id, dependencies);
1051         } else {
1052             dependencies.setMovedFrom(newParent.getNodeId());
1053         }
1054     }
1055 
1056     /**
1057      * Record the fact that the supplied node is in the process of being deleted, so any cached information (outside of the node
1058      * object itself) should be cleaned up.
1059      * 
1060      * @param node the node being deleted; never null
1061      */
1062     protected void recordDelete( Node<Payload, PropertyPayload> node ) {
1063         // Record the operation ...
1064         operations.delete(node.getLocation());
1065         // Fix the cache's state ...
1066         nodes.remove(node.getNodeId());
1067         changeDependencies.remove(node.getNodeId());
1068         recordUnloaded(node);
1069     }
1070 
1071     /**
1072      * Record the fact that the supplied node is in the process of being unloaded, so any cached information (outside of the node
1073      * object itself) should be cleaned up.
1074      * 
1075      * @param node the node being unloaded; never null
1076      */
1077     protected void recordUnloaded( final Node<Payload, PropertyPayload> node ) {
1078         if (node.isLoaded() && node.getChildrenCount() > 0) {
1079             // Walk the branch and remove all nodes from the map of all nodes ...
1080             node.onCachedNodes(new NodeVisitor<Payload, PropertyPayload>() {
1081                 @SuppressWarnings( "synthetic-access" )
1082                 @Override
1083                 public boolean visit( Node<Payload, PropertyPayload> unloaded ) {
1084                     if (unloaded != node) { // info for 'node' should not be removed
1085                         nodes.remove(unloaded.getNodeId());
1086                         changeDependencies.remove(unloaded.getNodeId());
1087                         unloaded.parent = null;
1088                     }
1089                     return true;
1090                 }
1091             });
1092         }
1093     }
1094 
1095     @ThreadSafe
1096     public static interface Operations<NodePayload, PropertyPayload> {
1097 
1098         /**
1099          * Update the children and properties for the node with the information from the persistent store.
1100          * 
1101          * @param persistentNode the persistent node that should be converted into a node info; never null
1102          * @param node the session's node representation that is to be updated; never null
1103          */
1104         void materialize( org.modeshape.graph.Node persistentNode,
1105                           Node<NodePayload, PropertyPayload> node );
1106 
1107         /**
1108          * Update the properties ONLY for the node with the information from the persistent store.
1109          * 
1110          * @param persistentNode the persistent node that should be converted into a node info; never null
1111          * @param node the session's node representation that is to be updated; never null
1112          */
1113         void materializeProperties( org.modeshape.graph.Node persistentNode,
1114                                     Node<NodePayload, PropertyPayload> node );
1115 
1116         /**
1117          * Signal that the node's {@link GraphSession.Node#getLocation() location} has been changed
1118          * 
1119          * @param node the node with the new location
1120          * @param oldLocation the old location of the node
1121          */
1122         void postUpdateLocation( Node<NodePayload, PropertyPayload> node,
1123                                  Location oldLocation );
1124 
1125         void preSetProperty( Node<NodePayload, PropertyPayload> node,
1126                              Name propertyName,
1127                              PropertyInfo<PropertyPayload> newProperty ) throws ValidationException;
1128 
1129         void postSetProperty( Node<NodePayload, PropertyPayload> node,
1130                               Name propertyName,
1131                               PropertyInfo<PropertyPayload> oldProperty );
1132 
1133         void preRemoveProperty( Node<NodePayload, PropertyPayload> node,
1134                                 Name propertyName ) throws ValidationException;
1135 
1136         void postRemoveProperty( Node<NodePayload, PropertyPayload> node,
1137                                  Name propertyName,
1138                                  PropertyInfo<PropertyPayload> oldProperty );
1139 
1140         /**
1141          * Notify that a new child with the supplied path segment is about to be created. When this method is called, the child
1142          * has not yet been added to the parent node.
1143          * 
1144          * @param parentNode the parent node; never null
1145          * @param newChild the path segment for the new child; never null
1146          * @param properties the initial properties for the new child, which can be manipulated directly; never null
1147          * @throws ValidationException if the parent may not have a child with the supplied name and the creation of the new node
1148          *         should be aborted
1149          */
1150         void preCreateChild( Node<NodePayload, PropertyPayload> parentNode,
1151                              Path.Segment newChild,
1152                              Map<Name, PropertyInfo<PropertyPayload>> properties ) throws ValidationException;
1153 
1154         /**
1155          * Notify that a new child has been added to the supplied parent node. The child may have an initial set of properties
1156          * specified at creation time, although none of the PropertyInfo objects will have a
1157          * {@link GraphSession.PropertyInfo#getPayload() payload}.
1158          * 
1159          * @param parentNode the parent node; never null
1160          * @param newChild the child that was just added to the parent node; never null
1161          * @param properties the properties of the child, which can be manipulated directly; never null
1162          * @throws ValidationException if the parent and child are not valid and the creation of the new node should be aborted
1163          */
1164         void postCreateChild( Node<NodePayload, PropertyPayload> parentNode,
1165                               Node<NodePayload, PropertyPayload> newChild,
1166                               Map<Name, PropertyInfo<PropertyPayload>> properties ) throws ValidationException;
1167 
1168         /**
1169          * Notify that an existing child will be moved from its current parent and placed under the supplied parent. When this
1170          * method is called, the child node has not yet been moved.
1171          * 
1172          * @param nodeToBeMoved the existing node that is to be moved from its current parent to the supplied parent; never null
1173          * @param newParentNode the new parent node; never null
1174          * @throws ValidationException if the child should not be moved
1175          */
1176         void preMove( Node<NodePayload, PropertyPayload> nodeToBeMoved,
1177                       Node<NodePayload, PropertyPayload> newParentNode ) throws ValidationException;
1178 
1179         /**
1180          * Notify that an existing child has been moved from the supplied previous parent into its new location. When this method
1181          * is called, the child node has been moved and any same-name-siblings that were after the child in the old parent have
1182          * had their SNS indexes adjusted.
1183          * 
1184          * @param movedNode the existing node that is was moved; never null
1185          * @param oldParentNode the old parent node; never null
1186          */
1187         void postMove( Node<NodePayload, PropertyPayload> movedNode,
1188                        Node<NodePayload, PropertyPayload> oldParentNode );
1189 
1190         /**
1191          * Notify that an existing child will be copied with the new copy being placed under the supplied parent. When this method
1192          * is called, the copy has not yet been performed.
1193          * 
1194          * @param original the existing node that is to be copied; never null
1195          * @param newParentNode the parent node where the copy is to be placed; never null
1196          * @throws ValidationException if the copy is not valid
1197          */
1198         void preCopy( Node<NodePayload, PropertyPayload> original,
1199                       Node<NodePayload, PropertyPayload> newParentNode ) throws ValidationException;
1200 
1201         /**
1202          * Notify that an existing child will be copied with the new copy being placed under the supplied parent. When this method
1203          * is called, the copy has been performed, but the new copy will not be loaded nor will be capable of being loaded.
1204          * 
1205          * @param original the original node that was copied; never null
1206          * @param copy the new copy that was made; never null
1207          */
1208         void postCopy( Node<NodePayload, PropertyPayload> original,
1209                        Node<NodePayload, PropertyPayload> copy );
1210 
1211         /**
1212          * Notify that an existing child will be removed from the supplied parent. When this method is called, the child node has
1213          * not yet been removed.
1214          * 
1215          * @param parentNode the parent node; never null
1216          * @param child the child that is to be removed from the parent node; never null
1217          * @throws ValidationException if the child should not be removed from the parent node
1218          */
1219         void preRemoveChild( Node<NodePayload, PropertyPayload> parentNode,
1220                              Node<NodePayload, PropertyPayload> child ) throws ValidationException;
1221 
1222         /**
1223          * Notify that an existing child has been removed from the supplied parent. When this method is called, the child node has
1224          * been removed and any same-name-siblings following the child have had their SNS indexes adjusted. Additionally, the
1225          * removed child no longer has a parent and is considered {@link GraphSession.Node#isStale() stale}.
1226          * 
1227          * @param parentNode the parent node; never null
1228          * @param removedChild the child that is to be removed from the parent node; never null
1229          */
1230         void postRemoveChild( Node<NodePayload, PropertyPayload> parentNode,
1231                               Node<NodePayload, PropertyPayload> removedChild );
1232 
1233         /**
1234          * Validate a node for consistency and well-formedness.
1235          * 
1236          * @param node the node to be validated
1237          * @param saveTime the time at which the save operation is occurring; never null
1238          * @throws ValidationException if there is a problem during validation
1239          */
1240         void preSave( Node<NodePayload, PropertyPayload> node,
1241                       DateTime saveTime ) throws ValidationException;
1242 
1243         /**
1244          * Update any computed fields based on the given node
1245          * 
1246          * @param batch the workspace graph batch in which computed fields should be created
1247          * @param node the node form which computed fields will be derived
1248          */
1249         void compute( Graph.Batch batch,
1250                       Node<NodePayload, PropertyPayload> node );
1251     }
1252 
1253     @ThreadSafe
1254     public static interface NodeIdFactory {
1255         NodeId create();
1256     }
1257 
1258     @ThreadSafe
1259     public static interface Authorizer {
1260 
1261         public enum Action {
1262             READ,
1263             REMOVE,
1264             ADD_NODE,
1265             SET_PROPERTY;
1266         }
1267 
1268         /**
1269          * Throws an {@link AccessControlException} if the current user is not able to perform the action on the node at the
1270          * supplied path in the current workspace.
1271          * 
1272          * @param path the path on which the actions are occurring
1273          * @param action the action to check
1274          * @throws AccessControlException if the user does not have permission to perform the actions
1275          */
1276         void checkPermissions( Path path,
1277                                Action action ) throws AccessControlException;
1278     }
1279 
1280     /**
1281      * {@link Authorizer} implementation that does nothing.
1282      */
1283     @ThreadSafe
1284     protected static class NoOpAuthorizer implements Authorizer {
1285         /**
1286          * {@inheritDoc}
1287          * 
1288          * @see org.modeshape.graph.session.GraphSession.Authorizer#checkPermissions(org.modeshape.graph.property.Path,
1289          *      org.modeshape.graph.session.GraphSession.Authorizer.Action)
1290          */
1291         public void checkPermissions( Path path,
1292                                       Action action ) throws AccessControlException {
1293         }
1294     }
1295 
1296     /**
1297      * A default implementation of {@link GraphSession.Operations} that provides all the basic functionality required by a graph
1298      * session. In this implementation, only the {@link GraphSession.NodeOperations#materialize(org.modeshape.graph.Node, Node)
1299      * materialize(...)} method does something.
1300      * 
1301      * @param <Payload> the type of node payload object
1302      * @param <PropertyPayload> the type of property payload object
1303      */
1304     @ThreadSafe
1305     public static class NodeOperations<Payload, PropertyPayload> implements Operations<Payload, PropertyPayload> {
1306         /**
1307          * {@inheritDoc}
1308          * 
1309          * @see GraphSession.Operations#materialize(org.modeshape.graph.Node, GraphSession.Node)
1310          */
1311         public void materialize( org.modeshape.graph.Node persistentNode,
1312                                  Node<Payload, PropertyPayload> node ) {
1313             // Create the map of property info objects ...
1314             Map<Name, PropertyInfo<PropertyPayload>> properties = new HashMap<Name, PropertyInfo<PropertyPayload>>();
1315             for (Property property : persistentNode.getProperties()) {
1316                 Name propertyName = property.getName();
1317                 PropertyInfo<PropertyPayload> info = new PropertyInfo<PropertyPayload>(property, property.isMultiple(),
1318                                                                                        Status.UNCHANGED, null);
1319                 properties.put(propertyName, info);
1320             }
1321             // Set only the children ...
1322             node.loadedWith(persistentNode.getChildren(), properties, persistentNode.getExpirationTime());
1323         }
1324 
1325         /**
1326          * {@inheritDoc}
1327          * 
1328          * @see GraphSession.Operations#materializeProperties(org.modeshape.graph.Node, GraphSession.Node)
1329          */
1330         public void materializeProperties( org.modeshape.graph.Node persistentNode,
1331                                            Node<Payload, PropertyPayload> node ) {
1332             // Create the map of property info objects ...
1333             Map<Name, PropertyInfo<PropertyPayload>> properties = new HashMap<Name, PropertyInfo<PropertyPayload>>();
1334             for (Property property : persistentNode.getProperties()) {
1335                 Name propertyName = property.getName();
1336                 PropertyInfo<PropertyPayload> info = new PropertyInfo<PropertyPayload>(property, property.isMultiple(),
1337                                                                                        Status.UNCHANGED, null);
1338                 properties.put(propertyName, info);
1339             }
1340             // Set only the children ...
1341             node.loadedWith(properties);
1342         }
1343 
1344         /**
1345          * {@inheritDoc}
1346          * 
1347          * @see GraphSession.Operations#postUpdateLocation(GraphSession.Node, org.modeshape.graph.Location)
1348          */
1349         public void postUpdateLocation( Node<Payload, PropertyPayload> node,
1350                                         Location oldLocation ) {
1351             // do nothing here
1352         }
1353 
1354         /**
1355          * {@inheritDoc}
1356          * 
1357          * @see GraphSession.Operations#preSave(GraphSession.Node,DateTime)
1358          */
1359         public void preSave( Node<Payload, PropertyPayload> node,
1360                              DateTime saveTime ) throws ValidationException {
1361             // do nothing here
1362         }
1363 
1364         /**
1365          * {@inheritDoc}
1366          * 
1367          * @see GraphSession.Operations#compute(Graph.Batch, GraphSession.Node)
1368          */
1369         public void compute( Graph.Batch batch,
1370                              Node<Payload, PropertyPayload> node ) {
1371             // do nothing here
1372         }
1373 
1374         /**
1375          * {@inheritDoc}
1376          * 
1377          * @see org.modeshape.graph.session.GraphSession.Operations#preSetProperty(Node, Name, PropertyInfo)
1378          */
1379         public void preSetProperty( Node<Payload, PropertyPayload> node,
1380                                     Name propertyName,
1381                                     PropertyInfo<PropertyPayload> newProperty ) throws ValidationException {
1382             // do nothing here
1383         }
1384 
1385         /**
1386          * {@inheritDoc}
1387          * 
1388          * @see org.modeshape.graph.session.GraphSession.Operations#postSetProperty(Node, Name, PropertyInfo)
1389          */
1390         public void postSetProperty( Node<Payload, PropertyPayload> node,
1391                                      Name propertyName,
1392                                      PropertyInfo<PropertyPayload> oldProperty ) {
1393             // do nothing here
1394         }
1395 
1396         /**
1397          * {@inheritDoc}
1398          * 
1399          * @see org.modeshape.graph.session.GraphSession.Operations#preRemoveProperty(Node, Name)
1400          */
1401         public void preRemoveProperty( Node<Payload, PropertyPayload> node,
1402                                        Name propertyName ) throws ValidationException {
1403             // do nothing here
1404         }
1405 
1406         /**
1407          * {@inheritDoc}
1408          * 
1409          * @see org.modeshape.graph.session.GraphSession.Operations#postRemoveProperty(Node, Name, PropertyInfo)
1410          */
1411         public void postRemoveProperty( Node<Payload, PropertyPayload> node,
1412                                         Name propertyName,
1413                                         PropertyInfo<PropertyPayload> oldProperty ) {
1414             // do nothing here
1415         }
1416 
1417         /**
1418          * {@inheritDoc}
1419          * 
1420          * @see org.modeshape.graph.session.GraphSession.Operations#preCreateChild(org.modeshape.graph.session.GraphSession.Node,
1421          *      org.modeshape.graph.property.Path.Segment, java.util.Map)
1422          */
1423         public void preCreateChild( Node<Payload, PropertyPayload> parent,
1424                                     Segment newChild,
1425                                     Map<Name, PropertyInfo<PropertyPayload>> properties ) throws ValidationException {
1426             // do nothing here
1427         }
1428 
1429         /**
1430          * {@inheritDoc}
1431          * 
1432          * @see org.modeshape.graph.session.GraphSession.Operations#postCreateChild(org.modeshape.graph.session.GraphSession.Node,
1433          *      org.modeshape.graph.session.GraphSession.Node, java.util.Map)
1434          */
1435         public void postCreateChild( Node<Payload, PropertyPayload> parent,
1436                                      Node<Payload, PropertyPayload> childChild,
1437                                      Map<Name, PropertyInfo<PropertyPayload>> properties ) throws ValidationException {
1438             // do nothing here
1439         }
1440 
1441         /**
1442          * {@inheritDoc}
1443          * 
1444          * @see org.modeshape.graph.session.GraphSession.Operations#preCopy(org.modeshape.graph.session.GraphSession.Node,
1445          *      org.modeshape.graph.session.GraphSession.Node)
1446          */
1447         public void preCopy( Node<Payload, PropertyPayload> original,
1448                              Node<Payload, PropertyPayload> newParent ) throws ValidationException {
1449         }
1450 
1451         /**
1452          * {@inheritDoc}
1453          * 
1454          * @see org.modeshape.graph.session.GraphSession.Operations#postCopy(org.modeshape.graph.session.GraphSession.Node,
1455          *      org.modeshape.graph.session.GraphSession.Node)
1456          */
1457         public void postCopy( Node<Payload, PropertyPayload> original,
1458                               Node<Payload, PropertyPayload> copy ) throws ValidationException {
1459         }
1460 
1461         /**
1462          * {@inheritDoc}
1463          * 
1464          * @see org.modeshape.graph.session.GraphSession.Operations#preMove(org.modeshape.graph.session.GraphSession.Node,
1465          *      org.modeshape.graph.session.GraphSession.Node)
1466          */
1467         public void preMove( Node<Payload, PropertyPayload> nodeToBeMoved,
1468                              Node<Payload, PropertyPayload> newParent ) throws ValidationException {
1469         }
1470 
1471         /**
1472          * {@inheritDoc}
1473          * 
1474          * @see org.modeshape.graph.session.GraphSession.Operations#postMove(org.modeshape.graph.session.GraphSession.Node,
1475          *      org.modeshape.graph.session.GraphSession.Node)
1476          */
1477         public void postMove( Node<Payload, PropertyPayload> movedNode,
1478                               Node<Payload, PropertyPayload> oldParent ) {
1479         }
1480 
1481         /**
1482          * {@inheritDoc}
1483          * 
1484          * @see org.modeshape.graph.session.GraphSession.Operations#preRemoveChild(org.modeshape.graph.session.GraphSession.Node,
1485          *      org.modeshape.graph.session.GraphSession.Node)
1486          */
1487         public void preRemoveChild( Node<Payload, PropertyPayload> parent,
1488                                     Node<Payload, PropertyPayload> newChild ) throws ValidationException {
1489             // do nothing here
1490         }
1491 
1492         /**
1493          * {@inheritDoc}
1494          * 
1495          * @see org.modeshape.graph.session.GraphSession.Operations#postRemoveChild(org.modeshape.graph.session.GraphSession.Node,
1496          *      org.modeshape.graph.session.GraphSession.Node)
1497          */
1498         public void postRemoveChild( Node<Payload, PropertyPayload> parent,
1499                                      Node<Payload, PropertyPayload> oldChild ) {
1500             // do nothing here
1501         }
1502     }
1503 
1504     @NotThreadSafe
1505     public static class Node<Payload, PropertyPayload> {
1506         private final GraphSession<Payload, PropertyPayload> cache;
1507         private final NodeId nodeId;
1508         private Node<Payload, PropertyPayload> parent;
1509         private long expirationTime = Long.MAX_VALUE;
1510         private Location location;
1511         private Status status = Status.UNCHANGED;
1512         private boolean changedBelow;
1513         private Map<Name, PropertyInfo<PropertyPayload>> properties;
1514         private ListMultimap<Name, Node<Payload, PropertyPayload>> childrenByName;
1515         private Payload payload;
1516 
1517         public Node( GraphSession<Payload, PropertyPayload> cache,
1518                      Node<Payload, PropertyPayload> parent,
1519                      NodeId nodeId,
1520                      Location location ) {
1521             this.cache = cache;
1522             this.parent = parent;
1523             this.nodeId = nodeId;
1524             this.location = location;
1525             assert this.cache != null;
1526             assert this.nodeId != null;
1527             assert this.location != null;
1528             assert this.location.hasPath();
1529         }
1530 
1531         /**
1532          * Get the session to which this node belongs.
1533          * 
1534          * @return the session; never null
1535          */
1536         public GraphSession<Payload, PropertyPayload> getSession() {
1537             return cache;
1538         }
1539 
1540         /**
1541          * Get the time when this node expires.
1542          * 
1543          * @return the time in milliseconds past the epoch when this node's cached information expires, or {@link Long#MAX_VALUE
1544          *         Long.MAX_VALUE} if there is no expiration or if the node has not been loaded
1545          * @see #isExpired()
1546          * @see #isLoaded()
1547          */
1548         public final long getExpirationTimeInMillis() {
1549             return expirationTime;
1550         }
1551 
1552         /**
1553          * Determine if this node's information has expired. This method will never return true if the node is not loaded. This
1554          * method is idempotent.
1555          * 
1556          * @return true if this node's information has been read from the store and is expired
1557          */
1558         public final boolean isExpired() {
1559             return expirationTime != Long.MAX_VALUE && expirationTime < cache.getCurrentTime();
1560         }
1561 
1562         /**
1563          * Determine if this node is loaded and usable. Even though the node may have been loaded previously, this method may
1564          * return false (and unloads the cached information) if the cached information has expired and thus is no longer usable.
1565          * Note, however, that changes on or below this node will prevent the node from being unloaded.
1566          * 
1567          * @return true if the node's information has already been loaded and may be used, or false otherwise
1568          */
1569         public final boolean isLoaded() {
1570             if (childrenByName == null) return false;
1571             // Otherwise, it is already loaded. First see if this is expired ...
1572             if (isExpired()) {
1573                 // It is expired, so we'd normally return false. But we should not unload if it has changes ...
1574                 if (isChanged(true)) return true;
1575                 // It is expired and contains no changes on this branch, so we can unload it ...
1576                 unload();
1577                 return false;
1578             }
1579             // Otherwise it is loaded and not expired ...
1580             return true;
1581         }
1582 
1583         /**
1584          * Method that causes the information for this node to be read from the store and loaded into the cache
1585          * 
1586          * @throws AccessControlException if the caller does not have the permission to perform the operation
1587          * @throws RepositorySourceException if there is a problem reading the store
1588          */
1589         protected final void load() throws RepositorySourceException {
1590             if (isLoaded()) return;
1591             assert !isStale();
1592             // If this node is new, then there's nothing to read ...
1593             if (status == Status.NEW) {
1594                 this.childrenByName = cache.NO_CHILDREN;
1595                 this.properties = cache.NO_PROPERTIES;
1596                 return;
1597             }
1598 
1599             // Check authorization before reading ...
1600             Path path = getPath();
1601             cache.authorizer.checkPermissions(path, Action.READ);
1602             int depth = cache.getDepthForLoadingNodes();
1603             if (depth == 1) {
1604                 // Then read the node from the store ...
1605                 org.modeshape.graph.Node persistentNode = cache.store.getNodeAt(getLocation());
1606                 // Check the actual location ...
1607                 Location actualLocation = persistentNode.getLocation();
1608                 if (!this.location.isSame(actualLocation)) {
1609                     // The actual location is changed, so update it ...
1610                     this.location = actualLocation;
1611                 }
1612                 // Update the persistent information ...
1613                 cache.nodeOperations.materialize(persistentNode, this);
1614             } else {
1615                 // Then read the node from the store ...
1616                 Subgraph subgraph = cache.store.getSubgraphOfDepth(depth).at(getLocation());
1617                 Location actualLocation = subgraph.getLocation();
1618                 if (!this.location.isSame(actualLocation)) {
1619                     // The actual location is changed, so update it ...
1620                     this.location = actualLocation;
1621                 }
1622                 // Update the persistent information ...
1623                 cache.nodeOperations.materialize(subgraph.getRoot(), this);
1624                 // Now update any nodes below this node ...
1625                 for (org.modeshape.graph.Node persistentNode : subgraph) {
1626                     // Find the node at the path ...
1627                     Path relativePath = persistentNode.getLocation().getPath().relativeTo(path);
1628                     Node<Payload, PropertyPayload> node = cache.findNodeRelativeTo(this, relativePath);
1629                     if (!node.isLoaded()) {
1630                         // Update the persistent information ...
1631                         cache.nodeOperations.materialize(persistentNode, node);
1632                     }
1633                 }
1634             }
1635         }
1636 
1637         /**
1638          * Utility method to unload this cached node.
1639          */
1640         protected final void unload() {
1641             assert !isStale();
1642             assert status == Status.UNCHANGED;
1643             assert !changedBelow;
1644             if (!isLoaded()) return;
1645             cache.recordUnloaded(this);
1646             childrenByName = null;
1647             expirationTime = Long.MAX_VALUE;
1648         }
1649 
1650         /**
1651          * Phase 1 of the process of refreshing the cached content while retaining changes. This phase walks the entire tree to
1652          * determine which nodes have changes, which nodes can be unloaded, and which nodes have no changes but are ancestors of
1653          * those nodes with changes (and therefore have to be refreshed). Each node has a {@link #isChanged(boolean) changed
1654          * state}, and the supplied RefreshState tracks which nodes must be
1655          * {@link GraphSession.RefreshState#markAsRequiringRefresh(Node) refreshed} in
1656          * {@link #refreshPhase2(RefreshState, Results) phase 2}; all other nodes are able to be unloaded in
1657          * {@link #refreshPhase2(RefreshState, Results) phase 2}.
1658          * 
1659          * @param refreshState the holder of the information about which nodes are to be unloaded or refreshed; may not be null
1660          * @return true if the node could be (or already is) unloaded, or false otherwise
1661          * @see #refreshPhase2(RefreshState, Results)
1662          */
1663         protected final boolean refreshPhase1( RefreshState<Payload, PropertyPayload> refreshState ) {
1664             assert !isStale();
1665             if (childrenByName == null) {
1666                 // This node is not yet loaded, so don't record it as needing to be unloaded but return true
1667                 return true;
1668             }
1669             // Perform phase 1 on each of the children ...
1670             boolean canUnloadChildren = true;
1671             for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
1672                 if (child.refreshPhase1(refreshState)) {
1673                     // The child can be unloaded
1674                     canUnloadChildren = false;
1675                 }
1676             }
1677 
1678             // If this node has changes, then we cannot do anything with this node ...
1679             if (isChanged(false)) return false;
1680 
1681             // Otherwise, this node contains no changes ...
1682             if (canUnloadChildren) {
1683                 // Since all the children can be unloaded, we can completely unload this node ...
1684                 return true;
1685             }
1686             // Otherwise, we have to hold onto the children, so we can't unload and must be refreshed ...
1687             refreshState.markAsRequiringRefresh(this);
1688             return false;
1689         }
1690 
1691         /**
1692          * Phase 2 of the process of refreshing the cached content while retaining changes. This phase walks the graph and either
1693          * unloads the node or, if the node is an ancestor of changed nodes, refreshes the node state to reflect that of the
1694          * persistent store.
1695          * 
1696          * @param refreshState
1697          * @param persistentInfoForRefreshedNodes
1698          * @see #refreshPhase1(RefreshState)
1699          */
1700         protected final void refreshPhase2( RefreshState<Payload, PropertyPayload> refreshState,
1701                                             Results persistentInfoForRefreshedNodes ) {
1702             assert !isStale();
1703             if (this.status != Status.UNCHANGED) {
1704                 // There are changes, so nothing to do ...
1705                 return;
1706             }
1707             if (refreshState.requiresRefresh(this)) {
1708                 // This node must be refreshed since it has no changes but is an ancestor of a node that is changed.
1709                 // Therefore, update the children and properties with the just-read persistent information ...
1710                 assert childrenByName != null;
1711                 org.modeshape.graph.Node persistentNode = persistentInfoForRefreshedNodes.getNode(location);
1712                 assert !persistentNode.getChildren().isEmpty();
1713 
1714                 // We need to keep the children that have been modified (or are ancestors of modified children),
1715                 // so build a list of the children that SHOULD NOT be replaced with the persistent info ...
1716                 Map<Location, Node<Payload, PropertyPayload>> childrenToKeep = new HashMap<Location, Node<Payload, PropertyPayload>>();
1717                 for (Node<Payload, PropertyPayload> existing : childrenByName.values()) {
1718                     if (existing.isChanged(true)) {
1719                         childrenToKeep.put(existing.getLocation(), existing);
1720                     } else {
1721                         // Otherwise, remove the child from the cache since we won't be needing it anymore ...
1722                         cache.nodes.remove(existing.getNodeId());
1723                         assert !cache.changeDependencies.containsKey(existing.getNodeId());
1724                         existing.parent = null;
1725                     }
1726                 }
1727 
1728                 // Now, clear the children ...
1729                 childrenByName.clear();
1730 
1731                 // And add the persistent children ...
1732                 for (Location location : persistentNode.getChildren()) {
1733                     Name childName = location.getPath().getLastSegment().getName();
1734                     List<Node<Payload, PropertyPayload>> currentChildren = childrenByName.get(childName);
1735                     // Find if there was an existing child that is supposed to stay ...
1736                     Node<Payload, PropertyPayload> existingChild = childrenToKeep.get(location);
1737                     if (existingChild != null) {
1738                         // The existing child is supposed to stay, since it has changes ...
1739                         currentChildren.add(existingChild);
1740                         if (currentChildren.size() != existingChild.getPath().getLastSegment().getIndex()) {
1741                             // Make sure the SNS index is correct ...
1742                             Path.Segment segment = cache.pathFactory.createSegment(childName, currentChildren.size());
1743                             existingChild.updateLocation(segment);
1744                             // TODO: Can the location be different? If so, doesn't that mean that the change requests
1745                             // have to be updated???
1746                         }
1747                     } else {
1748                         // The existing child (if there was one) is to be refreshed ...
1749                         NodeId nodeId = cache.idFactory.create();
1750                         Node<Payload, PropertyPayload> replacementChild = cache.createNode(this, nodeId, location);
1751                         cache.nodes.put(replacementChild.getNodeId(), replacementChild);
1752                         assert replacementChild.getName().equals(childName);
1753                         assert replacementChild.parent == this;
1754                         // Add it to the parent node ...
1755                         currentChildren.add(replacementChild);
1756                         // Create a segment with the SNS ...
1757                         Path.Segment segment = cache.pathFactory.createSegment(childName, currentChildren.size());
1758                         replacementChild.updateLocation(segment);
1759                     }
1760                 }
1761                 return;
1762             }
1763             // This node can be unloaded (since it has no changes and isn't above a node with changes) ...
1764             if (!this.changedBelow) unload();
1765         }
1766 
1767         /**
1768          * Define the persistent child information that this node is to be populated with. This method does not cause the node's
1769          * information to be read from the store.
1770          * <p>
1771          * This method is intended to be called by the {@link GraphSession.Operations#materialize(org.modeshape.graph.Node, Node)}
1772          * , and should not be called by other components.
1773          * </p>
1774          * 
1775          * @param children the children for this node; may not be null
1776          * @param properties the properties for this node; may not be null
1777          * @param expirationTime the time that this cached information expires, or null if there is no expiration
1778          */
1779         public void loadedWith( List<Location> children,
1780                                 Map<Name, PropertyInfo<PropertyPayload>> properties,
1781                                 DateTime expirationTime ) {
1782             assert !isStale();
1783             // Load the children ...
1784             if (children.isEmpty()) {
1785                 childrenByName = cache.NO_CHILDREN;
1786             } else {
1787                 childrenByName = LinkedListMultimap.create();
1788                 for (Location location : children) {
1789                     NodeId id = cache.idFactory.create();
1790                     Name childName = location.getPath().getLastSegment().getName();
1791                     Node<Payload, PropertyPayload> child = cache.createNode(this, id, location);
1792                     cache.nodes.put(child.getNodeId(), child);
1793                     List<Node<Payload, PropertyPayload>> currentChildren = childrenByName.get(childName);
1794                     currentChildren.add(child);
1795                     child.parent = this;
1796                     // Create a segment with the SNS ...
1797                     Path.Segment segment = cache.pathFactory.createSegment(childName, currentChildren.size());
1798                     child.updateLocation(segment);
1799                 }
1800             }
1801 
1802             loadedWith(properties);
1803 
1804             // Set the expiration time ...
1805             this.expirationTime = expirationTime != null ? expirationTime.getMilliseconds() : Long.MAX_VALUE;
1806         }
1807 
1808         /**
1809          * Define the persistent property information that this node is to be populated with. This method does not cause the
1810          * node's information to be read from the store.
1811          * 
1812          * @param properties the properties for this node; may not be null
1813          */
1814         public void loadedWith( Map<Name, PropertyInfo<PropertyPayload>> properties ) {
1815             // Load the properties ...
1816             if (properties.isEmpty()) {
1817                 this.properties = cache.NO_PROPERTIES;
1818             } else {
1819                 this.properties = new HashMap<Name, PropertyInfo<PropertyPayload>>(properties);
1820             }
1821         }
1822 
1823         /**
1824          * Reconstruct the location object for this node, given the information at the parent.
1825          * 
1826          * @param segment the path segment for this node; may be null only when this node is the root node
1827          */
1828         protected void updateLocation( Path.Segment segment ) {
1829             assert !isStale();
1830             Path newPath = null;
1831             Path currentPath = getPath();
1832             if (segment != null) {
1833                 if (segment.equals(currentPath.getLastSegment())) return;
1834                 // Recompute the path based upon the parent path ...
1835                 Path parentPath = getParent().getPath();
1836                 newPath = cache.pathFactory.create(parentPath, segment);
1837             } else {
1838                 if (this.isRoot()) return;
1839                 // This must be the root ...
1840                 newPath = cache.pathFactory.createRootPath();
1841                 assert this.isRoot();
1842             }
1843             Location newLocation = this.location.with(newPath);
1844             if (newLocation != this.location) {
1845                 Location oldLocation = this.location;
1846                 this.location = newLocation;
1847                 cache.nodeOperations.postUpdateLocation(this, oldLocation);
1848             }
1849 
1850             if (isLoaded() && childrenByName != cache.NO_CHILDREN) {
1851                 // Update all of the children ...
1852                 for (Map.Entry<Name, Collection<Node<Payload, PropertyPayload>>> entry : childrenByName.asMap().entrySet()) {
1853                     Name childName = entry.getKey();
1854                     int sns = 1;
1855                     for (Node<Payload, PropertyPayload> child : entry.getValue()) {
1856                         Path.Segment childSegment = cache.pathFactory.createSegment(childName, sns++);
1857                         child.updateLocation(childSegment);
1858                     }
1859                 }
1860             }
1861         }
1862 
1863         /**
1864          * This method is used to adjust the existing children by adding a child that was recently added to the persistent store
1865          * (via clone or copy). The new child will appear at the end of the existing children, but before any children that were
1866          * added to, moved into, created under this parent.
1867          * 
1868          * @param newChild the new child that was added
1869          */
1870         protected void synchronizeWithNewlyPersistedNode( Location newChild ) {
1871             if (!this.isLoaded()) return;
1872             Path childPath = newChild.getPath();
1873             Name childName = childPath.getLastSegment().getName();
1874             if (this.childrenByName.isEmpty()) {
1875                 // Just have to add the child ...
1876                 this.childrenByName = LinkedListMultimap.create();
1877                 if (childPath.getLastSegment().hasIndex()) {
1878                     // The child has a SNS index, but this is an only child ...
1879                     newChild = newChild.with(cache.pathFactory.create(childPath.getParent(), childName));
1880                 }
1881                 Node<Payload, PropertyPayload> child = cache.createNode(this, cache.idFactory.create(), newChild);
1882                 this.childrenByName.put(childName, child);
1883                 return;
1884             }
1885 
1886             // Unfortunately, there is no efficient way to insert into the multi-map, so we need to recreate it ...
1887             ListMultimap<Name, Node<Payload, PropertyPayload>> children = LinkedListMultimap.create();
1888             boolean added = false;
1889             for (Node<Payload, PropertyPayload> child : this.childrenByName.values()) {
1890                 if (!added && child.isNew()) {
1891                     // Add the new child here ...
1892                     Node<Payload, PropertyPayload> newChildNode = cache.createNode(this, cache.idFactory.create(), newChild);
1893                     children.put(childName, newChildNode);
1894                     added = true;
1895                 }
1896                 children.put(child.getName(), child);
1897             }
1898             if (!added) {
1899                 Node<Payload, PropertyPayload> newChildNode = cache.createNode(this, cache.idFactory.create(), newChild);
1900                 children.put(childName, newChildNode);
1901             }
1902 
1903             // Replace the children ...
1904             this.childrenByName = children;
1905 
1906             // Adjust the SNS indexes for those children with the same name as 'childToBeMoved' ...
1907             List<Node<Payload, PropertyPayload>> childrenWithName = childrenByName.get(childName);
1908             int snsIndex = 1;
1909             for (Node<Payload, PropertyPayload> sns : childrenWithName) {
1910                 if (sns.getSegment().getIndex() != snsIndex) {
1911                     // The SNS index is not correct, so fix it and update the location ...
1912                     Path.Segment newSegment = cache.pathFactory.createSegment(childName, snsIndex);
1913                     sns.updateLocation(newSegment);
1914                     sns.markAsChanged();
1915                 }
1916                 ++snsIndex;
1917             }
1918         }
1919 
1920         /**
1921          * Determine whether this node has been marked as having changes.
1922          * 
1923          * @param recursive true if the nodes under this node should be checked, or false if only this node should be checked
1924          * @return true if there are changes in the specified scope, or false otherwise
1925          */
1926         public final boolean isChanged( boolean recursive ) {
1927             if (this.status == Status.UNCHANGED) return recursive && this.changedBelow;
1928             return true;
1929         }
1930 
1931         /**
1932          * Determine whether this node has been created since the last save. If this method returns true, then by definition the
1933          * parent node will be marked as having {@link #isChanged(boolean) changed}.
1934          * 
1935          * @return true if this node is new, or false otherwise
1936          */
1937         public final boolean isNew() {
1938             return this.status == Status.NEW;
1939         }
1940 
1941         /**
1942          * This method determines whether this node, or any nodes below it, contain changes that depend on nodes that are outside
1943          * of this branch.
1944          * 
1945          * @return true if this branch has nodes with changes dependent on nodes outside of this branch
1946          */
1947         public boolean containsChangesWithExternalDependencies() {
1948             assert !isStale();
1949             if (!isChanged(true)) {
1950                 // There are no changes in this branch ...
1951                 return false;
1952             }
1953             // Need to make sure that nodes were not moved into or out of this branch, since that would mean that we
1954             // cannot refresh this branch without also refreshing the other affected branches (per the JCR specification) ...
1955             for (Map.Entry<NodeId, Dependencies> entry : cache.changeDependencies.entrySet()) {
1956                 Dependencies dependency = entry.getValue();
1957                 NodeId nodeId = entry.getKey();
1958                 Node<Payload, PropertyPayload> changedNode = cache.nodes.get(nodeId);
1959 
1960                 // First, check whether the changed node is within the branch ...
1961                 if (!changedNode.isAtOrBelow(this)) {
1962                     // The node is not within this branch, so the original parent must not be at or below this node ...
1963                     if (cache.nodes.get(dependency.getMovedFrom()).isAtOrBelow(this)) {
1964                         // The original parent is below 'this' but the changed node is not ...
1965                         return true;
1966                     }
1967                     // None of the other dependencies can be within this branch ...
1968                     for (NodeId dependentId : dependency.getRequireChangesTo()) {
1969                         // The dependent node must not be at or below this node ...
1970                         if (cache.nodes.get(dependentId).isAtOrBelow(this)) {
1971                             // The other node that must change is at or below 'this'
1972                             return true;
1973                         }
1974                     }
1975                     // Otherwise, continue with the next change ...
1976                     continue;
1977                 }
1978                 // The changed node is within this branch!
1979 
1980                 // Second, check whether this node was moved from outside this branch ...
1981                 if (dependency.getMovedFrom() != null) {
1982                     Node<Payload, PropertyPayload> originalParent = cache.nodes.get(dependency.getMovedFrom());
1983                     // If the original parent cannot be found ...
1984                     if (originalParent == null) {
1985                         continue;
1986                     }
1987                     // The original parent must be at or below this node ...
1988                     if (!originalParent.isAtOrBelow(this)) {
1989                         // The original parent is not within this branch (but the new parent is)
1990                         return true;
1991                     }
1992                     // All of the other dependencies must be within this branch ...
1993                     for (NodeId dependentId : dependency.getRequireChangesTo()) {
1994                         // The dependent node must not be at or below this node ...
1995                         if (!cache.nodes.get(dependentId).isAtOrBelow(this)) {
1996                             // Another dependent node is not at or below this branch either ...
1997                             return true;
1998                         }
1999                     }
2000                 }
2001             }
2002             return false;
2003         }
2004 
2005         /**
2006          * Clear any transient changes that have been accumulated in this node.
2007          * 
2008          * @see #markAsChanged()
2009          */
2010         public void clearChanges() {
2011             assert !isStale();
2012             if (this.status != Status.UNCHANGED) {
2013                 this.status = Status.UNCHANGED;
2014                 this.changedBelow = false;
2015                 unload();
2016             } else {
2017                 if (!this.changedBelow) return;
2018                 // This node has not changed but something below has, so call to the children ...
2019                 if (childrenByName != null && childrenByName != cache.NO_CHILDREN) {
2020                     for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
2021                         child.clearChanges();
2022                     }
2023                 }
2024                 this.changedBelow = false;
2025             }
2026             // Update the parent ...
2027             if (this.parent != null) this.parent.recomputeChangedBelow();
2028         }
2029 
2030         /**
2031          * Mark this node as having changes.
2032          * 
2033          * @see #clearChanges()
2034          * @see #markAsNew()
2035          */
2036         public final void markAsChanged() {
2037             assert !isStale();
2038             if (this.status == Status.NEW) return;
2039             this.status = Status.CHANGED;
2040             if (this.parent != null) this.parent.markAsChangedBelow();
2041         }
2042 
2043         public final void markAsCopied() {
2044             assert !isStale();
2045             this.status = Status.COPIED;
2046             if (this.parent != null) this.parent.markAsChangedBelow();
2047         }
2048 
2049         /**
2050          * Mark this node has having been created and not yet saved.
2051          * 
2052          * @see #clearChanges()
2053          * @see #markAsChanged()
2054          */
2055         public final void markAsNew() {
2056             assert !isStale();
2057             this.status = Status.NEW;
2058             if (this.parent != null) this.parent.markAsChanged();
2059         }
2060 
2061         protected final void markAsChangedBelow() {
2062             if (!this.changedBelow) {
2063                 this.changedBelow = true;
2064                 if (this.parent != null) this.parent.markAsChangedBelow();
2065             }
2066         }
2067 
2068         protected final void recomputeChangedBelow() {
2069             if (!this.changedBelow) return; // we're done
2070             // there are changes ...
2071             assert childrenByName != null;
2072             for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
2073                 if (child.isChanged(true)) {
2074                     this.markAsChangedBelow();
2075                     return;
2076                 }
2077             }
2078             // No changes found ...
2079             this.changedBelow = false;
2080             if (this.parent != null) this.parent.recomputeChangedBelow();
2081         }
2082 
2083         /**
2084          * Move this node from its current location so that is is a child of the supplied parent.
2085          * 
2086          * @param parent the new parent for this node; may not be null
2087          * @throws RepositorySourceException if the parent node is to be loaded but a problem is encountered while doing so
2088          * @throws IllegalArgumentException if this is the root node
2089          */
2090         public void moveTo( Node<Payload, PropertyPayload> parent ) {
2091             moveTo(parent, null, true);
2092         }
2093 
2094         /**
2095          * Move this node from its current location so that is is a child of the supplied parent, renaming the node in the
2096          * process.
2097          * 
2098          * @param parent the new parent for this node; may not be null
2099          * @param newNodeName the new name for the node, or null if the node should keep the same name
2100          * @throws RepositorySourceException if the parent node is to be loaded but a problem is encountered while doing so
2101          * @throws IllegalArgumentException if this is the root node
2102          */
2103         public void moveTo( Node<Payload, PropertyPayload> parent,
2104                             Name newNodeName ) {
2105             moveTo(parent, newNodeName, true);
2106         }
2107 
2108         /**
2109          * Move this node from its current location so that is is a child of the supplied parent.
2110          * 
2111          * @param parent the new parent for this node; may not be null
2112          * @param newNodeName the new name for the node, or null if the node should keep the same name
2113          * @param useBatch true if this operation should be performed using the session's current batch operation and executed
2114          *        upon {@link GraphSession#save()}, or false if the move should be performed immediately
2115          * @throws ValidationException if the supplied parent node is a decendant of this node
2116          * @throws RepositorySourceException if the parent node is to be loaded but a problem is encountered while doing so
2117          * @throws IllegalArgumentException if this is the root node
2118          * @throws AccessControlException if the caller does not have the permission to perform the operation
2119          */
2120         protected void moveTo( Node<Payload, PropertyPayload> parent,
2121                                Name newNodeName,
2122                                boolean useBatch ) {
2123             final Node<Payload, PropertyPayload> child = this;
2124             assert !parent.isStale();
2125             // Make sure the parent is not a decendant of the child ...
2126             if (parent.isAtOrBelow(child)) {
2127                 String path = cache.readable(getPath());
2128                 String parentPath = cache.readable(parent.getPath());
2129                 String workspaceName = cache.workspaceName;
2130                 String msg = GraphI18n.unableToMoveNodeToBeChildOfDecendent.text(path, parentPath, workspaceName);
2131                 throw new ValidationException(msg);
2132             }
2133 
2134             assert !child.isRoot();
2135             if (newNodeName == null) newNodeName = getName();
2136 
2137             // Check authorization ...
2138             cache.authorizer.checkPermissions(parent.getPath(), Action.ADD_NODE);
2139             cache.authorizer.checkPermissions(child.getPath().getParent(), Action.REMOVE);
2140 
2141             parent.load();
2142 
2143             cache.nodeOperations.preMove(child, parent);
2144 
2145             // Remove the child from it's existing parent ...
2146             final Node<Payload, PropertyPayload> oldParent = child.parent;
2147             // Record the operation ...
2148             if (useBatch) {
2149                 if (newNodeName.equals(getName())) {
2150                     cache.operations.move(child.getLocation()).into(parent.getLocation());
2151                 } else {
2152                     cache.operations.move(child.getLocation()).as(newNodeName).into(parent.getLocation());
2153                 }
2154             } else {
2155                 if (newNodeName.equals(getName())) {
2156                     cache.store.move(child.getLocation()).into(parent.getLocation());
2157                 } else {
2158                     cache.store.move(child.getLocation()).as(newNodeName).into(parent.getLocation());
2159                 }
2160             }
2161             // Remove the child from the current location (even if its the same node; there's cleanup to do) ...
2162             child.remove();
2163             // Now add the child ...
2164             if (parent.childrenByName == cache.NO_CHILDREN) {
2165                 parent.childrenByName = LinkedListMultimap.create();
2166             }
2167             parent.childrenByName.put(newNodeName, child);
2168             child.parent = parent;
2169             parent.markAsChanged();
2170             // Update the new child with the correct location ...
2171             int snsIndex = parent.childrenByName.get(newNodeName).size();
2172             Path.Segment segment = cache.pathFactory.createSegment(newNodeName, snsIndex);
2173             child.updateLocation(segment);
2174             cache.recordMove(child, oldParent, parent);
2175 
2176             cache.nodeOperations.postMove(child, oldParent);
2177         }
2178 
2179         /**
2180          * Rename this node to have a different name.
2181          * 
2182          * @param newNodeName
2183          */
2184         public void rename( Name newNodeName ) {
2185             moveTo(this.parent, newNodeName, true);
2186         }
2187 
2188         /**
2189          * Copy this node (and all nodes below it) and place the copy under the supplied parent location. The new copy will be
2190          * appended to any existing children of the supplied parent node, and will be given the appropriate same-name-sibling
2191          * index. This method may not be called on the root node.
2192          * 
2193          * @param parent the new parent for the new copy; may not be null
2194          * @throws RepositorySourceException if the parent node is to be loaded but a problem is encountered while doing so
2195          * @throws IllegalArgumentException if the parent is null, or if this is the root node
2196          * @throws AccessControlException if the caller does not have the permission to perform the operation
2197          */
2198         public void copyTo( Node<Payload, PropertyPayload> parent ) {
2199             CheckArg.isNotNull(parent, "parent");
2200             CheckArg.isEquals(this.isRoot(), "this.isRoot()", false, "false");
2201             final Node<Payload, PropertyPayload> child = this;
2202             assert !parent.isStale();
2203             assert child.parent != this;
2204             assert !child.isRoot();
2205 
2206             // Check authorization ...
2207             cache.authorizer.checkPermissions(parent.getPath(), Action.ADD_NODE);
2208             cache.authorizer.checkPermissions(child.getPath(), Action.READ);
2209 
2210             parent.load();
2211             if (parent.childrenByName == cache.NO_CHILDREN) {
2212                 parent.childrenByName = LinkedListMultimap.create();
2213             }
2214 
2215             cache.nodeOperations.preCopy(this, parent);
2216 
2217             Name childName = child.getName();
2218             // Figure out the name and SNS of the new copy ...
2219             List<Node<Payload, PropertyPayload>> currentChildren = parent.childrenByName.get(childName);
2220             Location copyLocation = Location.create(cache.pathFactory.create(parent.getPath(),
2221                                                                              childName,
2222                                                                              currentChildren.size() + 1));
2223 
2224             // Perform the copy ...
2225             cache.operations.copy(child.getLocation()).to(copyLocation);
2226 
2227             // Add the child to the parent ...
2228             Node<Payload, PropertyPayload> copy = cache.createNode(parent, cache.idFactory.create(), copyLocation);
2229             copy.markAsCopied(); // marks parent as changed
2230 
2231             cache.nodeOperations.postCopy(this, copy);
2232         }
2233 
2234         /**
2235          * Clone this node (and all nodes below it). The new copy will be appended to the existing children of the
2236          * {@link #getParent() parent}, and will be given the appropriate same-name-sibling index.
2237          * <p>
2238          * This is equivalent to calling <code>node.copyTo(node.getParent())</code>
2239          * </p>
2240          * 
2241          * @throws IllegalArgumentException if this is the root node
2242          */
2243         public void cloneNode() {
2244             copyTo(getParent());
2245         }
2246 
2247         /**
2248          * Move the specified child to be located immediately before the other supplied node.
2249          * 
2250          * @param childToBeMoved the path segment specifying the child that is to be moved
2251          * @param before the path segment of the node before which the {@code childToBeMoved} should be placed, or null if the
2252          *        child should be moved to the end
2253          * @throws PathNotFoundException if the <code>childToBeMoved</code> or <code>before</code> segments do not specify an
2254          *         existing child
2255          * @throws IllegalArgumentException if either segment is null or does not specify an existing node
2256          */
2257         public void orderChildBefore( Path.Segment childToBeMoved,
2258                                       Path.Segment before ) throws PathNotFoundException {
2259             CheckArg.isNotNull(childToBeMoved, "childToBeMoved");
2260 
2261             // Check authorization ...
2262             cache.authorizer.checkPermissions(getPath(), Action.REMOVE);
2263             cache.authorizer.checkPermissions(getPath(), Action.ADD_NODE);
2264 
2265             // Find the node to be moved ...
2266             Node<Payload, PropertyPayload> nodeToBeMoved = getChild(childToBeMoved);
2267             Node<Payload, PropertyPayload> beforeNode = before != null ? getChild(before) : null;
2268 
2269             if (beforeNode == null) {
2270                 // Moving the node into its parent will remove it from its current spot in the child list and re-add it to the end
2271                 cache.operations.move(nodeToBeMoved.getLocation()).into(this.location);
2272             } else {
2273                 // Record the move ...
2274                 cache.operations.move(nodeToBeMoved.getLocation()).before(beforeNode.getLocation());
2275             }
2276 
2277             // Unfortunately, there is no efficient way to insert into the multi-map, so we need to recreate it ...
2278             ListMultimap<Name, Node<Payload, PropertyPayload>> children = LinkedListMultimap.create();
2279             for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
2280                 if (child == nodeToBeMoved) continue;
2281                 if (before != null && child.getSegment().equals(before)) {
2282                     children.put(nodeToBeMoved.getName(), nodeToBeMoved);
2283                 }
2284                 children.put(child.getName(), child);
2285             }
2286             if (before == null) {
2287                 children.put(nodeToBeMoved.getName(), nodeToBeMoved);
2288             }
2289 
2290             // Replace the children ...
2291             this.childrenByName = children;
2292             this.markAsChanged();
2293 
2294             // Adjust the SNS indexes for those children with the same name as 'childToBeMoved' ...
2295             Name movedName = nodeToBeMoved.getName();
2296             List<Node<Payload, PropertyPayload>> childrenWithName = childrenByName.get(movedName);
2297             int snsIndex = 1;
2298             for (Node<Payload, PropertyPayload> sns : childrenWithName) {
2299                 if (sns.getSegment().getIndex() != snsIndex) {
2300                     // The SNS index is not correct, so fix it and update the location ...
2301                     Path.Segment newSegment = cache.pathFactory.createSegment(movedName, snsIndex);
2302                     sns.updateLocation(newSegment);
2303                     sns.markAsChanged();
2304                 }
2305                 ++snsIndex;
2306             }
2307         }
2308 
2309         /**
2310          * Remove this node from it's parent. Note that locations are <i>not</i> updated, since they will be updated if this node
2311          * is added to a different parent. However, the locations of same-name-siblings under the parent <i>are</i> updated.
2312          */
2313         protected void remove() {
2314             remove(true);
2315         }
2316 
2317         /**
2318          * Remove this node from it's parent. Note that locations are <i>not</i> updated, since they will be updated if this node
2319          * is added to a different parent. However, the locations of same-name-siblings under the parent <i>are</i> updated.
2320          * 
2321          * @param markParentAsChanged true if the parent should be marked as being changed (i.e., when changes are initiated from
2322          *        within this session), or false otherwise (i.e., when changes are made to reflect the persistent state)
2323          */
2324         protected void remove( boolean markParentAsChanged ) {
2325             assert !isStale();
2326             assert this.parent != null;
2327             assert this.parent.isLoaded();
2328             assert this.parent.childrenByName != null;
2329             assert this.parent.childrenByName != cache.NO_CHILDREN;
2330             if (markParentAsChanged) {
2331                 this.parent.markAsChanged();
2332                 this.markAsChanged();
2333             }
2334             Name name = getName();
2335             List<Node<Payload, PropertyPayload>> childrenWithSameName = this.parent.childrenByName.get(name);
2336             this.parent = null;
2337             if (childrenWithSameName.size() == 1) {
2338                 // No same-name-siblings ...
2339                 childrenWithSameName.clear();
2340             } else {
2341                 // There is at least one other sibling with the same name ...
2342                 int lastIndex = childrenWithSameName.size() - 1;
2343                 assert lastIndex > 0;
2344                 int index = childrenWithSameName.indexOf(this);
2345                 // remove this node ...
2346                 childrenWithSameName.remove(index);
2347                 if (index != lastIndex) {
2348                     // There are same-name-siblings that have higher SNS indexes that this node had ...
2349                     for (int i = index; i != lastIndex; ++i) {
2350                         Node<Payload, PropertyPayload> sibling = childrenWithSameName.get(i);
2351                         Path.Segment segment = cache.pathFactory.createSegment(name, i + 1);
2352                         sibling.updateLocation(segment);
2353                     }
2354                 }
2355             }
2356         }
2357 
2358         /**
2359          * Remove this node from it's parent and destroy it's contents. The location of sibling nodes with the same name will be
2360          * updated, and the node and all nodes below it will be destroyed and removed from the cache.
2361          * 
2362          * @throws AccessControlException if the caller does not have the permission to perform the operation
2363          */
2364         public void destroy() {
2365             assert !isStale();
2366             // Check authorization ...
2367             cache.authorizer.checkPermissions(getPath(), Action.REMOVE);
2368 
2369             final Node<Payload, PropertyPayload> parent = this.parent;
2370             cache.nodeOperations.preRemoveChild(parent, this);
2371             // Remove the node from its parent ...
2372             remove();
2373             // This node was successfully removed, so now remove it from the cache ...
2374             cache.recordDelete(this);
2375             cache.nodeOperations.postRemoveChild(parent, this);
2376         }
2377 
2378         public final boolean isRoot() {
2379             return this.parent == null;
2380         }
2381 
2382         /**
2383          * Determine whether this node is stale because it was dropped from the cache.
2384          * 
2385          * @return true if the node is stale and should no longer be used
2386          */
2387         public boolean isStale() {
2388             // Find the root of this node ...
2389             Node<?, ?> node = this;
2390             while (node.parent != null) {
2391                 node = node.parent;
2392             }
2393             // The root of this branch MUST be the actual root of the cache
2394             return node != cache.root;
2395         }
2396 
2397         /**
2398          * Get this node's parent node.
2399          * 
2400          * @return the parent node
2401          */
2402         public Node<Payload, PropertyPayload> getParent() {
2403             assert !isStale();
2404             return parent;
2405         }
2406 
2407         /**
2408          * @return nodeId
2409          */
2410         public final NodeId getNodeId() {
2411             return nodeId;
2412         }
2413 
2414         /**
2415          * Get the name of this node, without any same-name-sibling index.
2416          * 
2417          * @return the name; never null
2418          */
2419         public Name getName() {
2420             return location.getPath().getLastSegment().getName();
2421         }
2422 
2423         /**
2424          * Get the {@link Path.Segment path segment} for this node.
2425          * 
2426          * @return the path segment; never null
2427          */
2428         public final Path.Segment getSegment() {
2429             return location.getPath().getLastSegment();
2430         }
2431 
2432         /**
2433          * Get the current path to this node.
2434          * 
2435          * @return the current path; never null
2436          */
2437         public final Path getPath() {
2438             return location.getPath();
2439         }
2440 
2441         /**
2442          * Get the current location for this node.
2443          * 
2444          * @return the current location; never null
2445          */
2446         public final Location getLocation() {
2447             return location;
2448         }
2449 
2450         /**
2451          * Create a new child node with the supplied name. The same-name-sibling index will be determined based upon the existing
2452          * children.
2453          * 
2454          * @param name the name of the new child node
2455          * @return the new child node
2456          * @throws IllegalArgumentException if the name is null
2457          * @throws RepositorySourceException if this node must be loaded but doing so results in a problem
2458          */
2459         public Node<Payload, PropertyPayload> createChild( Name name ) {
2460             CheckArg.isNotNull(name, "name");
2461             return doCreateChild(name, null, null);
2462         }
2463 
2464         /**
2465          * Create a new child node with the supplied name and multiple initial properties. The same-name-sibling index will be
2466          * determined based upon the existing children.
2467          * 
2468          * @param name the name of the new child node
2469          * @param properties the (non-identification) properties for the new node
2470          * @return the new child node
2471          * @throws IllegalArgumentException if the name or properties are null
2472          * @throws ValidationException if the new node is not valid as a child
2473          * @throws RepositorySourceException if this node must be loaded but doing so results in a problem
2474          */
2475         public Node<Payload, PropertyPayload> createChild( Name name,
2476                                                            Property... properties ) {
2477             CheckArg.isNotNull(name, "name");
2478             CheckArg.isNotNull(properties, "properties");
2479             return doCreateChild(name, null, properties);
2480         }
2481 
2482         /**
2483          * Create a new child node with the supplied name and multiple initial identification properties. The same-name-sibling
2484          * index will be determined based upon the existing children.
2485          * 
2486          * @param name the name of the new child node
2487          * @param idProperties the identification properties for the new node
2488          * @return the new child node
2489          * @throws IllegalArgumentException if the name or properties are null
2490          * @throws ValidationException if the new node is not valid as a child
2491          * @throws RepositorySourceException if this node must be loaded but doing so results in a problem
2492          */
2493         public Node<Payload, PropertyPayload> createChild( Name name,
2494                                                            Collection<Property> idProperties ) {
2495             CheckArg.isNotNull(name, "name");
2496             CheckArg.isNotEmpty(idProperties, "idProperties");
2497             return doCreateChild(name, idProperties, null);
2498         }
2499 
2500         /**
2501          * Create a new child node with the supplied name and multiple initial properties. The same-name-sibling index will be
2502          * determined based upon the existing children.
2503          * 
2504          * @param name the name of the new child node
2505          * @param idProperties the identification properties for the new node
2506          * @param remainingProperties the remaining (non-identification) properties for the new node
2507          * @return the new child node
2508          * @throws IllegalArgumentException if the name or properties are null
2509          * @throws ValidationException if the new node is not valid as a child
2510          * @throws RepositorySourceException if this node must be loaded but doing so results in a problem
2511          */
2512         public Node<Payload, PropertyPayload> createChild( Name name,
2513                                                            Collection<Property> idProperties,
2514                                                            Property... remainingProperties ) {
2515             CheckArg.isNotNull(name, "name");
2516             CheckArg.isNotEmpty(idProperties, "idProperties");
2517             return doCreateChild(name, idProperties, remainingProperties);
2518         }
2519 
2520         private Node<Payload, PropertyPayload> doCreateChild( Name name,
2521                                                               Collection<Property> idProperties,
2522                                                               Property[] remainingProperties ) throws ValidationException {
2523             assert !isStale();
2524 
2525             // Check permission here ...
2526             Path path = getPath();
2527             cache.authorizer.checkPermissions(path, Action.ADD_NODE);
2528 
2529             // Now load if required ...
2530             load();
2531 
2532             // Figure out the name and SNS of the new copy ...
2533             List<Node<Payload, PropertyPayload>> currentChildren = childrenByName.get(name);
2534             Path newPath = cache.pathFactory.create(path, name, currentChildren.size() + 1);
2535             Location newChild = idProperties != null && !idProperties.isEmpty() ? Location.create(newPath, idProperties) : Location.create(newPath);
2536 
2537             // Create the properties ...
2538             Map<Name, PropertyInfo<PropertyPayload>> newProperties = new HashMap<Name, PropertyInfo<PropertyPayload>>();
2539             if (idProperties != null) {
2540                 for (Property idProp : idProperties) {
2541                     PropertyInfo<PropertyPayload> info = new PropertyInfo<PropertyPayload>(idProp, idProp.isMultiple(),
2542                                                                                            Status.NEW, null);
2543                     newProperties.put(info.getName(), info);
2544                 }
2545             }
2546             if (remainingProperties != null) {
2547                 for (Property property : remainingProperties) {
2548                     PropertyInfo<PropertyPayload> info2 = new PropertyInfo<PropertyPayload>(property, property.isMultiple(),
2549                                                                                             Status.NEW, null);
2550                     newProperties.put(info2.getName(), info2);
2551                 }
2552             }
2553 
2554             // Notify before the addition ...
2555             cache.nodeOperations.preCreateChild(this, newPath.getLastSegment(), newProperties);
2556 
2557             // Record the current state before any changes ...
2558             Status statusBefore = this.status;
2559             boolean changedBelowBefore = this.changedBelow;
2560 
2561             // Add the child to the parent ...
2562             Node<Payload, PropertyPayload> child = cache.createNode(this, cache.idFactory.create(), newChild);
2563             child.markAsNew(); // marks parent as changed
2564             if (childrenByName == cache.NO_CHILDREN) {
2565                 childrenByName = LinkedListMultimap.create();
2566             }
2567             childrenByName.put(name, child);
2568 
2569             // Set the properties on the new node, but in a private backdoor way ...
2570             assert child.properties == null;
2571             child.properties = newProperties;
2572             child.childrenByName = cache.NO_CHILDREN;
2573 
2574             try {
2575                 // The node has been changed, so try notifying before we record the creation (which can't be undone) ...
2576                 cache.nodeOperations.postCreateChild(this, child, child.properties);
2577 
2578                 // Notification was fine, so now do the create ...
2579                 Graph.Create<Graph.Batch> create = cache.operations.create(newChild.getPath());
2580                 if (!child.properties.isEmpty()) {
2581                     // Process the property infos (in case some were added during the pre- or post- operations ...
2582                     for (PropertyInfo<PropertyPayload> property : child.properties.values()) {
2583                         create.with(property.getProperty());
2584                     }
2585                 }
2586                 create.and();
2587             } catch (ValidationException e) {
2588                 // Clean up the children ...
2589                 if (childrenByName.size() == 1) {
2590                     childrenByName = cache.NO_CHILDREN;
2591                 } else {
2592                     childrenByName.remove(child.getName(), child);
2593                 }
2594                 this.status = statusBefore;
2595                 this.changedBelow = changedBelowBefore;
2596                 throw e;
2597             }
2598 
2599             cache.nodes.put(child.getNodeId(), child);
2600 
2601             return child;
2602         }
2603 
2604         /**
2605          * Determine whether this node has a child with the supplied name and SNS index.
2606          * 
2607          * @param segment the segment of the child
2608          * @return true if there is a child, or false if there is no such child
2609          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2610          */
2611         public boolean hasChild( Path.Segment segment ) {
2612             return hasChild(segment.getName(), segment.getIndex());
2613         }
2614 
2615         /**
2616          * Determine whether this node has a child with the supplied name and SNS index.
2617          * 
2618          * @param name the name of the child
2619          * @param sns the same-name-sibling index; must be 1 or more
2620          * @return true if there is a child, or false if there is no such child
2621          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2622          */
2623         public boolean hasChild( Name name,
2624                                  int sns ) {
2625             load();
2626             List<Node<Payload, PropertyPayload>> children = childrenByName.get(name); // never null
2627             return children.size() >= sns; // SNS is 1-based, index is 0-based
2628         }
2629 
2630         /**
2631          * Get the child with the supplied segment.
2632          * 
2633          * @param segment the segment of the child
2634          * @return the child with the supplied name and SNS index, or null if the children have not yet been loaded
2635          * @throws PathNotFoundException if the children have been loaded and the child does not exist
2636          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2637          */
2638         public Node<Payload, PropertyPayload> getChild( Path.Segment segment ) {
2639             return getChild(segment.getName(), segment.getIndex());
2640         }
2641 
2642         /**
2643          * Get the first child matching the name and lowest SNS index
2644          * 
2645          * @param name the name of the child
2646          * @return the first child with the supplied name, or null if the children have not yet been loaded
2647          * @throws PathNotFoundException if the children have been loaded and the child does not exist
2648          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2649          */
2650         public Node<Payload, PropertyPayload> getFirstChild( Name name ) {
2651             return getChild(name, 1);
2652         }
2653 
2654         /**
2655          * Get the child with the supplied name and SNS index.
2656          * 
2657          * @param name the name of the child
2658          * @param sns the same-name-sibling index; must be 1 or more
2659          * @return the child with the supplied name and SNS index; never null
2660          * @throws PathNotFoundException if the children have been loaded and the child does not exist
2661          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2662          */
2663         public Node<Payload, PropertyPayload> getChild( Name name,
2664                                                         int sns ) {
2665             load();
2666             List<Node<Payload, PropertyPayload>> children = childrenByName.get(name); // never null
2667             try {
2668                 return children.get(sns - 1); // SNS is 1-based, index is 0-based
2669             } catch (IndexOutOfBoundsException e) {
2670                 Path missingPath = cache.pathFactory.create(getPath(), name, sns);
2671                 throw new PathNotFoundException(Location.create(missingPath), getPath());
2672             }
2673         }
2674 
2675         /**
2676          * Get an iterator over the children that have the supplied name.
2677          * 
2678          * @param name the of the child nodes to be returned; may not be null
2679          * @return an unmodifiable iterator over the cached children that have the supplied name; never null but possibly empty
2680          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2681          */
2682         public Iterable<Node<Payload, PropertyPayload>> getChildren( Name name ) {
2683             load();
2684             final Collection<Node<Payload, PropertyPayload>> children = childrenByName.get(name);
2685             return new Iterable<Node<Payload, PropertyPayload>>() {
2686                 public Iterator<Node<Payload, PropertyPayload>> iterator() {
2687                     return new ReadOnlyIterator<Node<Payload, PropertyPayload>>(children.iterator());
2688                 }
2689             };
2690         }
2691 
2692         /**
2693          * Get an iterator over the children.
2694          * 
2695          * @return an unmodifiable iterator over the cached children; never null
2696          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2697          */
2698         public Iterable<Node<Payload, PropertyPayload>> getChildren() {
2699             load();
2700             final Collection<Node<Payload, PropertyPayload>> children = childrenByName.values();
2701             return new Iterable<Node<Payload, PropertyPayload>>() {
2702                 public Iterator<Node<Payload, PropertyPayload>> iterator() {
2703                     return new ReadOnlyIterator<Node<Payload, PropertyPayload>>(children.iterator());
2704                 }
2705             };
2706         }
2707 
2708         /**
2709          * Get the number of children.
2710          * 
2711          * @return the number of children in the cache
2712          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2713          */
2714         public int getChildrenCount() {
2715             load();
2716             return childrenByName.size();
2717         }
2718 
2719         /**
2720          * Get the number of children that have the same supplied name.
2721          * 
2722          * @param name the name of the children to count
2723          * @return the number of children in the cache
2724          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2725          */
2726         public int getChildrenCount( Name name ) {
2727             load();
2728             return childrenByName.get(name).size();
2729         }
2730 
2731         /**
2732          * Determine if this node is a leaf node with no children.
2733          * 
2734          * @return true if this node has no children
2735          * @throws RepositorySourceException if there is a problem loading this node's information from the store
2736          */
2737         public boolean isLeaf() {
2738             load();
2739             return childrenByName.isEmpty();
2740         }
2741 
2742         /**
2743          * Get from this node the property with the supplied name.
2744          * 
2745          * @param name the property name; may not be null
2746          * @return the property with the supplied name, or null if there is no such property on this node
2747          */
2748         public PropertyInfo<PropertyPayload> getProperty( Name name ) {
2749             load();
2750             return properties.get(name);
2751         }
2752 
2753         /**
2754          * Set the supplied property information on this node.
2755          * 
2756          * @param property the new property; may not be null
2757          * @param isMultiValued true if the property is multi-valued
2758          * @param payload the optional payload for this property; may be null
2759          * @return the previous information for the property, or null if there was no previous property
2760          * @throws AccessControlException if the caller does not have the permission to perform the operation
2761          */
2762         public PropertyInfo<PropertyPayload> setProperty( Property property,
2763                                                           boolean isMultiValued,
2764                                                           PropertyPayload payload ) {
2765             assert !isStale();
2766             cache.authorizer.checkPermissions(getPath(), Action.SET_PROPERTY);
2767 
2768             load();
2769             if (properties == cache.NO_PROPERTIES) {
2770                 properties = new HashMap<Name, PropertyInfo<PropertyPayload>>();
2771             }
2772 
2773             Name name = property.getName();
2774             PropertyInfo<PropertyPayload> previous = properties.get(name);
2775             Status status = null;
2776             if (previous != null) {
2777                 status = previous.getStatus(); // keep NEW or CHANGED status, but UNCHANGED -> CHANGED
2778                 if (status == Status.UNCHANGED) status = Status.CHANGED;
2779             } else {
2780                 status = Status.NEW;
2781             }
2782             PropertyInfo<PropertyPayload> info = new PropertyInfo<PropertyPayload>(property, isMultiValued, status, payload);
2783             cache.nodeOperations.preSetProperty(this, property.getName(), info);
2784             properties.put(name, info);
2785             cache.operations.set(property).on(location);
2786             markAsChanged();
2787             cache.nodeOperations.postSetProperty(this, property.getName(), previous);
2788             return previous;
2789         }
2790 
2791         /**
2792          * Remove a property from this node.
2793          * 
2794          * @param name the name of the property to be removed; may not be null
2795          * @return the previous information for the property, or null if there was no previous property
2796          */
2797         public PropertyInfo<PropertyPayload> removeProperty( Name name ) {
2798             assert !isStale();
2799             cache.authorizer.checkPermissions(getPath(), Action.REMOVE);
2800 
2801             load();
2802             if (!properties.containsKey(name)) return null;
2803             cache.nodeOperations.preRemoveProperty(this, name);
2804             PropertyInfo<PropertyPayload> results = properties.remove(name);
2805             markAsChanged();
2806             cache.operations.remove(name).on(location);
2807             cache.nodeOperations.postRemoveProperty(this, name, results);
2808             return results;
2809         }
2810 
2811         /**
2812          * Get the names of the properties on this node.
2813          * 
2814          * @return the names of the properties; never null
2815          */
2816         public Set<Name> getPropertyNames() {
2817             load();
2818             return properties.keySet();
2819         }
2820 
2821         /**
2822          * Get the information for each of the properties on this node.
2823          * 
2824          * @return the information for each of the properties; never null
2825          */
2826         public Collection<PropertyInfo<PropertyPayload>> getProperties() {
2827             load();
2828             return properties.values();
2829         }
2830 
2831         /**
2832          * Get the number of properties owned by this node.
2833          * 
2834          * @return the number of properties; never negative
2835          */
2836         public int getPropertyCount() {
2837             load();
2838             return properties.size();
2839         }
2840 
2841         public boolean isAtOrBelow( Node<Payload, PropertyPayload> other ) {
2842             Node<Payload, PropertyPayload> node = this;
2843             while (node != null) {
2844                 if (node == other) return true;
2845                 node = node.getParent();
2846             }
2847             return false;
2848         }
2849 
2850         /**
2851          * @return payload
2852          */
2853         public Payload getPayload() {
2854             load();
2855             return payload;
2856         }
2857 
2858         /**
2859          * @param payload Sets payload to the specified value.
2860          */
2861         public void setPayload( Payload payload ) {
2862             this.payload = payload;
2863         }
2864 
2865         /**
2866          * {@inheritDoc}
2867          * 
2868          * @see java.lang.Object#hashCode()
2869          */
2870         @Override
2871         public final int hashCode() {
2872             return nodeId.hashCode();
2873         }
2874 
2875         /**
2876          * {@inheritDoc}
2877          * 
2878          * @see java.lang.Object#equals(java.lang.Object)
2879          */
2880         @SuppressWarnings( "unchecked" )
2881         @Override
2882         public boolean equals( Object obj ) {
2883             if (obj == this) return true;
2884             if (obj instanceof Node) {
2885                 Node<Payload, PropertyPayload> that = (Node<Payload, PropertyPayload>)obj;
2886                 if (this.isStale() || that.isStale()) return false;
2887                 if (!this.nodeId.equals(that.nodeId)) return false;
2888                 return this.location.isSame(that.location);
2889             }
2890             return false;
2891         }
2892 
2893         /**
2894          * Utility method to obtain a string representation that uses the namespace prefixes where appropriate.
2895          * 
2896          * @param registry the namespace registry, or null if no prefixes should be used
2897          * @return the string representation; never null
2898          */
2899         public String getString( NamespaceRegistry registry ) {
2900             return "Cached node <" + nodeId + "> at " + location.getString(registry);
2901         }
2902 
2903         /**
2904          * {@inheritDoc}
2905          * 
2906          * @see java.lang.Object#toString()
2907          */
2908         @Override
2909         public String toString() {
2910             return getString(null);
2911         }
2912 
2913         /**
2914          * Visit all nodes in the cache that are already loaded
2915          * 
2916          * @param visitor the visitor; may not be null
2917          */
2918         public void onLoadedNodes( NodeVisitor<Payload, PropertyPayload> visitor ) {
2919             if (this.isLoaded()) {
2920                 // Create a queue. This queue will contain all the nodes to be visited,
2921                 // so if loading is not forced, then the queue should only contain already-loaded nodes...
2922                 LinkedList<Node<Payload, PropertyPayload>> queue = new LinkedList<Node<Payload, PropertyPayload>>();
2923                 queue.add(this);
2924                 while (!queue.isEmpty()) {
2925                     Node<Payload, PropertyPayload> node = queue.poll();
2926                     // Get an iterator over the children *before* we visit the node
2927                     Iterator<Node<Payload, PropertyPayload>> iter = node.getChildren().iterator();
2928                     // Visit this node ...
2929                     if (visitor.visit(node)) {
2930                         // Visit the children ...
2931                         int index = -1;
2932                         while (iter.hasNext()) {
2933                             Node<Payload, PropertyPayload> child = iter.next();
2934                             if (child.isLoaded()) {
2935                                 queue.add(++index, child);
2936                             }
2937                         }
2938                     }
2939                 }
2940             }
2941             visitor.finish();
2942         }
2943 
2944         /**
2945          * Visit all loaded and unloaded nodes in the cache.
2946          * 
2947          * @param visitor the visitor; may not be null
2948          */
2949         public void onCachedNodes( NodeVisitor<Payload, PropertyPayload> visitor ) {
2950             // Create a queue. This queue will contain all the nodes to be visited,
2951             // so if loading is not forced, then the queue should only contain already-loaded nodes...
2952             LinkedList<Node<Payload, PropertyPayload>> queue = new LinkedList<Node<Payload, PropertyPayload>>();
2953             queue.add(this);
2954             while (!queue.isEmpty()) {
2955                 Node<Payload, PropertyPayload> node = queue.poll();
2956                 if (!node.isLoaded()) {
2957                     visitor.visit(node);
2958                     continue;
2959                 }
2960                 // Get an iterator over the children *before* we visit the node
2961                 Iterator<Node<Payload, PropertyPayload>> iter = node.getChildren().iterator();
2962                 // Visit this node ...
2963                 if (visitor.visit(node)) {
2964                     // Visit the children ...
2965                     int index = -1;
2966                     while (iter.hasNext()) {
2967                         Node<Payload, PropertyPayload> child = iter.next();
2968                         queue.add(++index, child);
2969                     }
2970                 }
2971             }
2972             visitor.finish();
2973         }
2974 
2975         /**
2976          * Visit all changed nodes in the cache.
2977          * 
2978          * @param visitor the visitor; may not be null
2979          */
2980         public void onChangedNodes( NodeVisitor<Payload, PropertyPayload> visitor ) {
2981             if (this.isChanged(true)) {
2982                 // Create a queue. This queue will contain all the nodes to be visited ...
2983                 LinkedList<Node<Payload, PropertyPayload>> changedNodes = new LinkedList<Node<Payload, PropertyPayload>>();
2984                 changedNodes.add(this);
2985                 while (!changedNodes.isEmpty()) {
2986                     Node<Payload, PropertyPayload> node = changedNodes.poll();
2987                     // Visit this node ...
2988                     boolean visitChildren = true;
2989                     if (node.isChanged(false)) {
2990                         visitChildren = visitor.visit(node);
2991                     }
2992                     if (visitChildren && node.isChanged(true)) {
2993                         // Visit the children ...
2994                         int index = -1;
2995                         Iterator<Node<Payload, PropertyPayload>> iter = node.getChildren().iterator();
2996                         while (iter.hasNext()) {
2997                             Node<Payload, PropertyPayload> child = iter.next();
2998                             if (node.isChanged(true)) {
2999                                 changedNodes.add(++index, child);
3000                             }
3001                         }
3002                     }
3003                 }
3004             }
3005             visitor.finish();
3006         }
3007 
3008         /**
3009          * Obtain a snapshot of the structure below this node.
3010          * 
3011          * @param pathsOnly true if the snapshot should only include paths, or false if the entire locations should be included
3012          * @return the snapshot
3013          */
3014         public StructureSnapshot<PropertyPayload> getSnapshot( final boolean pathsOnly ) {
3015             final List<Snapshot<PropertyPayload>> snapshots = new ArrayList<Snapshot<PropertyPayload>>();
3016             onCachedNodes(new NodeVisitor<Payload, PropertyPayload>() {
3017                 @Override
3018                 public boolean visit( Node<Payload, PropertyPayload> node ) {
3019                     snapshots.add(new Snapshot<PropertyPayload>(node, pathsOnly, true));
3020                     return node.isLoaded();
3021                 }
3022             });
3023             return new StructureSnapshot<PropertyPayload>(cache.context().getNamespaceRegistry(),
3024                                                           Collections.unmodifiableList(snapshots));
3025         }
3026     }
3027 
3028     public static enum Status {
3029         NEW,
3030         CHANGED,
3031         UNCHANGED,
3032         COPIED;
3033     }
3034 
3035     public static final class PropertyInfo<PropertyPayload> {
3036         private final Property property;
3037         private final Status status;
3038         private final boolean multiValued;
3039         private final PropertyPayload payload;
3040 
3041         public PropertyInfo( Property property,
3042                              boolean multiValued,
3043                              Status status,
3044                              PropertyPayload payload ) {
3045             assert property != null;
3046             assert status != null;
3047             this.property = property;
3048             this.status = status;
3049             this.multiValued = multiValued;
3050             this.payload = payload;
3051         }
3052 
3053         /**
3054          * Get the status of this property.
3055          * 
3056          * @return the current status; never null
3057          */
3058         public Status getStatus() {
3059             return status;
3060         }
3061 
3062         /**
3063          * Determine whether this property has been modified since it was last saved.
3064          * 
3065          * @return true if the {@link #getStatus() status} is {@link Status#CHANGED changed}
3066          */
3067         public boolean isModified() {
3068             return status != Status.UNCHANGED && status != Status.NEW;
3069         }
3070 
3071         /**
3072          * Determine whether this property has been created since the last save.
3073          * 
3074          * @return true if the {@link #getStatus() status} is {@link Status#NEW new}
3075          */
3076         public boolean isNew() {
3077             return status == Status.NEW;
3078         }
3079 
3080         /**
3081          * Get the name of the property.
3082          * 
3083          * @return the propert name; never null
3084          */
3085         public Name getName() {
3086             return property.getName();
3087         }
3088 
3089         /**
3090          * Get the Graph API property object containing the values.
3091          * 
3092          * @return the property object; never null
3093          */
3094         public Property getProperty() {
3095             return property;
3096         }
3097 
3098         /**
3099          * Get the payload for this property.
3100          * 
3101          * @return the payload; may be null if there is no payload
3102          */
3103         public PropertyPayload getPayload() {
3104             return payload;
3105         }
3106 
3107         /**
3108          * Determine whether this property has multiple values
3109          * 
3110          * @return multiValued
3111          */
3112         public boolean isMultiValued() {
3113             return multiValued;
3114         }
3115 
3116         /**
3117          * {@inheritDoc}
3118          * 
3119          * @see java.lang.Object#hashCode()
3120          */
3121         @Override
3122         public int hashCode() {
3123             return getName().hashCode();
3124         }
3125 
3126         /**
3127          * {@inheritDoc}
3128          * 
3129          * @see java.lang.Object#equals(java.lang.Object)
3130          */
3131         @Override
3132         public boolean equals( Object obj ) {
3133             if (obj == this) return true;
3134             if (obj instanceof PropertyInfo<?>) {
3135                 PropertyInfo<?> that = (PropertyInfo<?>)obj;
3136                 return getName().equals(that.getName());
3137             }
3138             return false;
3139         }
3140 
3141         /**
3142          * {@inheritDoc}
3143          * 
3144          * @see java.lang.Object#toString()
3145          */
3146         @Override
3147         public String toString() {
3148             StringBuilder sb = new StringBuilder();
3149             sb.append(getName());
3150             // if (payload != null) sb.append(payload);
3151             if (property.isSingle()) {
3152                 sb.append(" with value ");
3153             } else {
3154                 sb.append(" with values ");
3155             }
3156             sb.append(Arrays.asList(property.getValuesAsArray()));
3157             return sb.toString();
3158         }
3159     }
3160 
3161     /**
3162      * The node visitor.
3163      * 
3164      * @param <NodePayload> the type of node payload object
3165      * @param <PropertyPayloadType> the type of property payload object
3166      */
3167     @NotThreadSafe
3168     public static abstract class NodeVisitor<NodePayload, PropertyPayloadType> {
3169         /**
3170          * Visit the supplied node, returning whether the children should be visited.
3171          * 
3172          * @param node the node to be visited; never null
3173          * @return true if the node's children should be visited, or false if no children should be visited
3174          */
3175         public abstract boolean visit( Node<NodePayload, PropertyPayloadType> node );
3176 
3177         /**
3178          * Method that should be called after all visiting has been done successfully (with no exceptions), including when no
3179          * nodes were visited.
3180          */
3181         public void finish() {
3182         }
3183     }
3184 
3185     /**
3186      * An abstract base class for visitors that need to load nodes using a single batch for all read operations. To use, simply
3187      * subclass and supply a {@link #visit(Node)} implementation that calls {@link #load(Node)} for each node that is to be
3188      * loaded. When the visitor is {@link #finish() finished}, all of these nodes will be read from the store and loaded. The
3189      * {@link #finishNodeAfterLoading(Node)} is called after each node is loaded, allowing the subclass to perform an operation on
3190      * the newly-loaded nodes.
3191      */
3192     @NotThreadSafe
3193     protected abstract class LoadNodesVisitor extends NodeVisitor<Payload, PropertyPayload> {
3194         private Graph.Batch batch = GraphSession.this.store.batch();
3195         private List<Node<Payload, PropertyPayload>> nodesToLoad = new LinkedList<Node<Payload, PropertyPayload>>();
3196 
3197         /**
3198          * Method that signals that the supplied node should be loaded (if it is not already loaded). This method should be called
3199          * from within the {@link #visit(Node)} method of the subclass.
3200          * 
3201          * @param node the node that should be loaded (if it is not already)
3202          */
3203         protected void load( Node<Payload, PropertyPayload> node ) {
3204             if (node != null && !node.isLoaded() && !node.isNew()) {
3205                 nodesToLoad.add(node);
3206                 batch.read(node.getLocation());
3207             }
3208         }
3209 
3210         /**
3211          * {@inheritDoc}
3212          * 
3213          * @see GraphSession.NodeVisitor#finish()
3214          */
3215         @Override
3216         public void finish() {
3217             super.finish();
3218             if (!nodesToLoad.isEmpty()) {
3219                 // Read all of the children in one batch ...
3220                 Results results = batch.execute();
3221                 // Now load all of the children into the correct node ...
3222                 for (Node<Payload, PropertyPayload> childToBeRead : nodesToLoad) {
3223                     org.modeshape.graph.Node persistentNode = results.getNode(childToBeRead.getLocation());
3224                     nodeOperations.materialize(persistentNode, childToBeRead);
3225                     finishNodeAfterLoading(childToBeRead);
3226                 }
3227             }
3228         }
3229 
3230         /**
3231          * Method that is called on each node loaded by this visitor. This method does nothing by default.
3232          * 
3233          * @param node the just-loaded node; never null
3234          */
3235         protected void finishNodeAfterLoading( Node<Payload, PropertyPayload> node ) {
3236             // do nothing
3237         }
3238     }
3239 
3240     /**
3241      * A visitor that ensures that all children of a node are loaded, and provides a hook to {@link #finishNodeAfterLoading(Node)
3242      * post-process the parent}.
3243      */
3244     @NotThreadSafe
3245     protected class LoadAllChildrenVisitor extends LoadNodesVisitor {
3246         private List<Node<Payload, PropertyPayload>> parentsVisited = new LinkedList<Node<Payload, PropertyPayload>>();
3247 
3248         /**
3249          * {@inheritDoc}
3250          * 
3251          * @see GraphSession.NodeVisitor#visit(GraphSession.Node)
3252          */
3253         @Override
3254         public boolean visit( Node<Payload, PropertyPayload> node ) {
3255             parentsVisited.add(node);
3256             Iterator<Node<Payload, PropertyPayload>> iter = node.getChildren().iterator();
3257             while (iter.hasNext()) {
3258                 load(iter.next());
3259             }
3260             return true;
3261         }
3262 
3263         /**
3264          * {@inheritDoc}
3265          * 
3266          * @see GraphSession.LoadNodesVisitor#finish()
3267          */
3268         @Override
3269         public void finish() {
3270             super.finish();
3271             for (Node<Payload, PropertyPayload> parent : parentsVisited) {
3272                 finishParentAfterLoading(parent);
3273             }
3274         }
3275 
3276         /**
3277          * Method that is called at the end of the {@link #finish()} stage with each parent node whose children were all loaded.
3278          * 
3279          * @param parentNode the parent of the just-loaded children; never null
3280          */
3281         protected void finishParentAfterLoading( Node<Payload, PropertyPayload> parentNode ) {
3282             // do nothing
3283         }
3284     }
3285 
3286     protected static final class Snapshot<PropertyPayload> {
3287         private final Location location;
3288         private final boolean isLoaded;
3289         private final boolean isChanged;
3290         private final Collection<PropertyInfo<PropertyPayload>> properties;
3291         private final NodeId id;
3292 
3293         protected Snapshot( Node<?, PropertyPayload> node,
3294                             boolean pathsOnly,
3295                             boolean includeProperties ) {
3296             this.location = pathsOnly && node.getLocation().hasIdProperties() ? Location.create(node.getLocation().getPath()) : node.getLocation();
3297             this.isLoaded = node.isLoaded();
3298             this.isChanged = node.isChanged(false);
3299             this.id = node.getNodeId();
3300             this.properties = includeProperties ? node.getProperties() : null;
3301         }
3302 
3303         /**
3304          * @return location
3305          */
3306         public Location getLocation() {
3307             return location;
3308         }
3309 
3310         /**
3311          * @return isChanged
3312          */
3313         public boolean isChanged() {
3314             return isChanged;
3315         }
3316 
3317         /**
3318          * @return isLoaded
3319          */
3320         public boolean isLoaded() {
3321             return isLoaded;
3322         }
3323 
3324         /**
3325          * @return id
3326          */
3327         public NodeId getId() {
3328             return id;
3329         }
3330 
3331         /**
3332          * @return properties
3333          */
3334         public Collection<PropertyInfo<PropertyPayload>> getProperties() {
3335             return properties;
3336         }
3337     }
3338 
3339     /**
3340      * A read-only visitor that walks the cache to obtain a snapshot of the cache structure. The resulting snapshot contains the
3341      * location of each node in the tree, including unloaded nodes.
3342      * 
3343      * @param <PropertyPayload> the property payload
3344      */
3345     @Immutable
3346     public static final class StructureSnapshot<PropertyPayload> implements Iterable<Snapshot<PropertyPayload>> {
3347         private final List<Snapshot<PropertyPayload>> snapshotsInPreOrder;
3348         private final NamespaceRegistry registry;
3349 
3350         protected StructureSnapshot( NamespaceRegistry registry,
3351                                      List<Snapshot<PropertyPayload>> snapshotsInPreOrder ) {
3352             assert snapshotsInPreOrder != null;
3353             this.snapshotsInPreOrder = snapshotsInPreOrder;
3354             this.registry = registry;
3355         }
3356 
3357         /**
3358          * {@inheritDoc}
3359          * 
3360          * @see java.lang.Iterable#iterator()
3361          */
3362         public Iterator<Snapshot<PropertyPayload>> iterator() {
3363             return snapshotsInPreOrder.iterator();
3364         }
3365 
3366         /**
3367          * Get the Location for every node in this cache
3368          * 
3369          * @return the node locations (in pre-order)
3370          */
3371         public List<Snapshot<PropertyPayload>> getSnapshotsInPreOrder() {
3372             return snapshotsInPreOrder;
3373         }
3374 
3375         /**
3376          * {@inheritDoc}
3377          * 
3378          * @see java.lang.Object#toString()
3379          */
3380         @Override
3381         public String toString() {
3382             int maxLength = 0;
3383             for (Snapshot<PropertyPayload> snapshot : this) {
3384                 String path = snapshot.getLocation().getPath().getString(registry);
3385                 maxLength = Math.max(maxLength, path.length());
3386             }
3387             StringBuilder sb = new StringBuilder();
3388             for (Snapshot<PropertyPayload> snapshot : this) {
3389                 Location location = snapshot.getLocation();
3390                 sb.append(StringUtil.justifyLeft(location.getPath().getString(registry), maxLength, ' '));
3391                 // Append the node identifier ...
3392                 sb.append(StringUtil.justifyRight(snapshot.getId().toString(), 10, ' '));
3393                 // Append the various state flags
3394                 if (snapshot.isChanged()) sb.append(" (*)");
3395                 else if (!snapshot.isLoaded()) sb.append(" (-)");
3396                 else sb.append("    ");
3397                 // Append the location's identifier properties ...
3398                 if (location.hasIdProperties()) {
3399                     sb.append("  ");
3400                     if (location.getIdProperties().size() == 1 && location.getUuid() != null) {
3401                         sb.append(location.getUuid());
3402                     } else {
3403                         boolean first = true;
3404                         sb.append('[');
3405                         for (Property property : location) {
3406                             sb.append(property.getString(registry));
3407                             if (first) first = false;
3408                             else sb.append(", ");
3409                         }
3410                         sb.append(']');
3411                     }
3412                 }
3413                 // Append the property information ...
3414                 if (snapshot.getProperties() != null) {
3415                     boolean first = true;
3416                     sb.append("  {");
3417                     for (PropertyInfo<?> info : snapshot.getProperties()) {
3418                         if (first) first = false;
3419                         else sb.append("} {");
3420                         sb.append(info.getProperty().getString(registry));
3421                     }
3422                     sb.append("}");
3423                 }
3424                 sb.append("\n");
3425             }
3426             return sb.toString();
3427         }
3428     }
3429 
3430     @NotThreadSafe
3431     protected static final class RefreshState<Payload, PropertyPayload> {
3432         private final Set<Node<Payload, PropertyPayload>> refresh = new HashSet<Node<Payload, PropertyPayload>>();
3433 
3434         public void markAsRequiringRefresh( Node<Payload, PropertyPayload> node ) {
3435             refresh.add(node);
3436         }
3437 
3438         public boolean requiresRefresh( Node<Payload, PropertyPayload> node ) {
3439             return refresh.contains(node);
3440         }
3441 
3442         public Set<Node<Payload, PropertyPayload>> getNodesToBeRefreshed() {
3443             return refresh;
3444         }
3445     }
3446 
3447     @NotThreadSafe
3448     protected final static class Dependencies {
3449         private Set<NodeId> requireChangesTo;
3450         private NodeId movedFrom;
3451 
3452         public Dependencies() {
3453         }
3454 
3455         /**
3456          * @return movedFrom
3457          */
3458         public NodeId getMovedFrom() {
3459             return movedFrom;
3460         }
3461 
3462         /**
3463          * Record that this node is being moved from one parent to another. This method only records the original parent, so
3464          * subsequent calls to this method do nothing.
3465          * 
3466          * @param movedFrom the identifier of the original parent of this node
3467          */
3468         public void setMovedFrom( NodeId movedFrom ) {
3469             if (this.movedFrom == null) this.movedFrom = movedFrom;
3470         }
3471 
3472         /**
3473          * @return requireChangesTo
3474          */
3475         public Set<NodeId> getRequireChangesTo() {
3476             return requireChangesTo != null ? requireChangesTo : Collections.<NodeId>emptySet();
3477         }
3478 
3479         /**
3480          * @param other the other node that changes are dependent upon
3481          */
3482         public void addRequireChangesTo( NodeId other ) {
3483             if (other == null) return;
3484             if (requireChangesTo == null) {
3485                 requireChangesTo = new HashSet<NodeId>();
3486             }
3487             requireChangesTo.add(other);
3488         }
3489     }
3490 
3491     /**
3492      * An immutable identifier for a node, used within the {@link GraphSession}.
3493      */
3494     @Immutable
3495     public final static class NodeId {
3496 
3497         private final long nodeId;
3498 
3499         /**
3500          * Create a new node identifier.
3501          * 
3502          * @param nodeId unique identifier
3503          */
3504         public NodeId( long nodeId ) {
3505             this.nodeId = nodeId;
3506         }
3507 
3508         /**
3509          * {@inheritDoc}
3510          * 
3511          * @see java.lang.Object#hashCode()
3512          */
3513         @Override
3514         public int hashCode() {
3515             return (int)nodeId;
3516         }
3517 
3518         /**
3519          * {@inheritDoc}
3520          * 
3521          * @see java.lang.Object#equals(java.lang.Object)
3522          */
3523         @Override
3524         public boolean equals( Object obj ) {
3525             if (obj == this) return true;
3526             if (obj instanceof NodeId) {
3527                 NodeId that = (NodeId)obj;
3528                 return this.nodeId == that.nodeId;
3529             }
3530             return false;
3531         }
3532 
3533         /**
3534          * {@inheritDoc}
3535          * 
3536          * @see java.lang.Object#toString()
3537          */
3538         @Override
3539         public String toString() {
3540             return Long.toString(nodeId);
3541         }
3542     }
3543 
3544 }