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 }