Author: rhauch
Date: 2009-07-09 12:23:09 -0400 (Thu, 09 Jul 2009)
New Revision: 1081
Added:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/GraphSession.java
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/InvalidStateException.java
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/ValidationException.java
trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/
trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/GraphCacheTest.java
Modified:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/Graph.java
trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java
trunk/dna-graph/src/main/java/org/jboss/dna/graph/Node.java
trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/PathNotFoundException.java
trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties
Log:
DNA-466 Added new session-based system to the Graph API.
Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/Graph.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/Graph.java 2009-07-09 16:22:06 UTC
(rev 1080)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/Graph.java 2009-07-09 16:23:09 UTC
(rev 1081)
@@ -41,6 +41,7 @@
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.NotThreadSafe;
import org.jboss.dna.common.collection.EmptyIterator;
@@ -63,6 +64,7 @@
import org.jboss.dna.graph.property.ValueFormatException;
import org.jboss.dna.graph.property.Path.Segment;
import org.jboss.dna.graph.request.BatchRequestBuilder;
+import org.jboss.dna.graph.request.CacheableRequest;
import org.jboss.dna.graph.request.CloneWorkspaceRequest;
import org.jboss.dna.graph.request.CompositeRequest;
import org.jboss.dna.graph.request.CreateNodeRequest;
@@ -1819,13 +1821,15 @@
}
public List<Location> under( Location at ) {
- return requests.readBlockOfChildren(at,
getCurrentWorkspaceName(), startingIndex, blockSize).getChildren();
+ return requests.readBlockOfChildren(at,
getCurrentWorkspaceName(), startingIndex, blockSize)
+ .getChildren();
}
};
}
public List<Location> startingAfter( final Location
previousSibling ) {
- return requests.readNextBlockOfChildren(previousSibling,
getCurrentWorkspaceName(), blockSize).getChildren();
+ return requests.readNextBlockOfChildren(previousSibling,
getCurrentWorkspaceName(), blockSize)
+ .getChildren();
}
public List<Location> startingAfter( String
pathOfPreviousSibling ) {
@@ -3726,7 +3730,7 @@
assertNotExecuted();
return new At<BatchConjunction>() {
public BatchConjunction at( Location location ) {
- requestQueue.readBranch(location, getCurrentWorkspaceName());
+ requestQueue.readBranch(location, getCurrentWorkspaceName(), depth);
return Batch.this.nextRequests;
}
@@ -3833,6 +3837,11 @@
return value;
}
+ protected static DateTime computeExpirationTime( CacheableRequest request ) {
+ CachePolicy policy = request.getCachePolicy();
+ return policy == null ? null :
request.getTimeLoaded().plus(policy.getTimeToLive(), TimeUnit.MILLISECONDS);
+ }
+
/**
* The interface used to specify the name of a new workspace.
*/
@@ -5363,6 +5372,11 @@
return request.getActualLocationOfNode();
}
+ public DateTime getExpirationTime() {
+ CachePolicy policy = request.getCachePolicy();
+ return policy == null ? null :
request.getTimeLoaded().plus(policy.getTimeToLive(), TimeUnit.MILLISECONDS);
+ }
+
public Graph getGraph() {
return Graph.this;
}
@@ -5431,24 +5445,29 @@
for (Request request : requests) {
if (request instanceof ReadAllPropertiesRequest) {
ReadAllPropertiesRequest read = (ReadAllPropertiesRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).setProperties(read.getPropertiesByName());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).setProperties(read.getPropertiesByName());
} else if (request instanceof ReadPropertyRequest) {
ReadPropertyRequest read = (ReadPropertyRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).addProperty(read.getProperty());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).addProperty(read.getProperty());
} else if (request instanceof ReadNodeRequest) {
ReadNodeRequest read = (ReadNodeRequest)request;
- BatchResultsNode node =
getOrCreateNode(read.getActualLocationOfNode());
+ DateTime expires = computeExpirationTime(read);
+ BatchResultsNode node =
getOrCreateNode(read.getActualLocationOfNode(), expires);
node.setProperties(read.getPropertiesByName());
node.setChildren(read.getChildren());
} else if (request instanceof ReadBlockOfChildrenRequest) {
throw new IllegalStateException();
} else if (request instanceof ReadAllChildrenRequest) {
ReadAllChildrenRequest read = (ReadAllChildrenRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).setChildren(read.getChildren());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).setChildren(read.getChildren());
} else if (request instanceof ReadBranchRequest) {
ReadBranchRequest read = (ReadBranchRequest)request;
+ DateTime expires = computeExpirationTime(read);
for (Location location : read) {
- BatchResultsNode node = getOrCreateNode(location);
+ BatchResultsNode node = getOrCreateNode(location, expires);
node.setProperties(read.getPropertiesFor(location));
node.setChildren(read.getChildren(location));
}
@@ -5462,24 +5481,29 @@
/*package*/BatchResults( Request request ) {
if (request instanceof ReadAllPropertiesRequest) {
ReadAllPropertiesRequest read = (ReadAllPropertiesRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).setProperties(read.getPropertiesByName());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).setProperties(read.getPropertiesByName());
} else if (request instanceof ReadPropertyRequest) {
ReadPropertyRequest read = (ReadPropertyRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).addProperty(read.getProperty());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).addProperty(read.getProperty());
} else if (request instanceof ReadNodeRequest) {
ReadNodeRequest read = (ReadNodeRequest)request;
- BatchResultsNode node = getOrCreateNode(read.getActualLocationOfNode());
+ DateTime expires = computeExpirationTime(read);
+ BatchResultsNode node = getOrCreateNode(read.getActualLocationOfNode(),
expires);
node.setProperties(read.getPropertiesByName());
node.setChildren(read.getChildren());
} else if (request instanceof ReadBlockOfChildrenRequest) {
throw new IllegalStateException();
} else if (request instanceof ReadAllChildrenRequest) {
ReadAllChildrenRequest read = (ReadAllChildrenRequest)request;
-
getOrCreateNode(read.getActualLocationOfNode()).setChildren(read.getChildren());
+ DateTime expires = computeExpirationTime(read);
+ getOrCreateNode(read.getActualLocationOfNode(),
expires).setChildren(read.getChildren());
} else if (request instanceof ReadBranchRequest) {
ReadBranchRequest read = (ReadBranchRequest)request;
+ DateTime expires = computeExpirationTime(read);
for (Location location : read) {
- BatchResultsNode node = getOrCreateNode(location);
+ BatchResultsNode node = getOrCreateNode(location, expires);
node.setProperties(read.getPropertiesFor(location));
node.setChildren(read.getChildren(location));
}
@@ -5492,10 +5516,11 @@
/*package*/BatchResults() {
}
- private BatchResultsNode getOrCreateNode( Location location ) {
+ private BatchResultsNode getOrCreateNode( Location location,
+ DateTime expirationTime ) {
BatchResultsNode node = nodes.get(location);
if (node == null) {
- node = new BatchResultsNode(location);
+ node = new BatchResultsNode(location, expirationTime);
assert location.getPath() != null;
nodes.put(location.getPath(), node);
}
@@ -5566,13 +5591,20 @@
@Immutable
class BatchResultsNode implements Node {
private final Location location;
+ private final DateTime expirationTime;
private Map<Name, Property> properties;
private List<Location> children;
- BatchResultsNode( Location location ) {
+ BatchResultsNode( Location location,
+ DateTime expirationTime ) {
this.location = location;
+ this.expirationTime = expirationTime;
}
+ public DateTime getExpirationTime() {
+ return expirationTime;
+ }
+
void addProperty( Property property ) {
if (this.properties == null) this.properties = new HashMap<Name,
Property>();
this.properties.put(property.getName(), property);
@@ -5739,7 +5771,10 @@
}
public SubgraphNode getNode( Name relativePath ) {
- Path path =
getGraph().getContext().getValueFactories().getPathFactory().create(getLocation().getPath(),
relativePath);
+ Path path = getGraph().getContext()
+ .getValueFactories()
+ .getPathFactory()
+ .create(getLocation().getPath(), relativePath);
path = path.getNormalizedPath();
return getNode(path);
}
@@ -5777,6 +5812,10 @@
this.request = request;
}
+ public DateTime getExpirationTime() {
+ return computeExpirationTime(request);
+ }
+
public List<Location> getChildren() {
List<Location> children = request.getChildren(location);
if (children == null) children = NO_CHILDREN;
Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java 2009-07-09 16:22:06
UTC (rev 1080)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java 2009-07-09 16:23:09
UTC (rev 1081)
@@ -121,6 +121,11 @@
public static I18n moveLimitedToBeWithinSingleSource;
public static I18n cloneLimitedToBeWithinSingleSource;
+ /* Session */
+ public static I18n
unableToRefreshBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
+ public static I18n
unableToSaveBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
+ public static I18n nodeHasAlreadyBeenRemovedFromThisSession;
+
static {
try {
I18n.initialize(GraphI18n.class);
Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/Node.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/Node.java 2009-07-09 16:22:06 UTC
(rev 1080)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/Node.java 2009-07-09 16:23:09 UTC
(rev 1081)
@@ -27,6 +27,7 @@
import java.util.List;
import java.util.Map;
import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.property.DateTime;
import org.jboss.dna.graph.property.Name;
import org.jboss.dna.graph.property.Path;
import org.jboss.dna.graph.property.Property;
@@ -47,6 +48,13 @@
Graph getGraph();
/**
+ * Get the time at which this node representation should no longer be used.
+ *
+ * @return the expiration time, or null if there is none
+ */
+ DateTime getExpirationTime();
+
+ /**
* Get the location of the node.
*
* @return the node's location
Modified:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/PathNotFoundException.java
===================================================================
---
trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/PathNotFoundException.java 2009-07-09
16:22:06 UTC (rev 1080)
+++
trunk/dna-graph/src/main/java/org/jboss/dna/graph/property/PathNotFoundException.java 2009-07-09
16:23:09 UTC (rev 1081)
@@ -105,8 +105,8 @@
@Override
public String getMessage() {
if (this.lowestAncestorThatDoesExist != null) {
- return
GraphI18n.pathNotFoundExceptionLowestExistingLocationFound.text(super.getMessage(),
-
this.lowestAncestorThatDoesExist);
+ String locationPart = location.hasPath() ? location.getPath().toString() :
location.toString();
+ return
GraphI18n.pathNotFoundExceptionLowestExistingLocationFound.text(locationPart,
this.lowestAncestorThatDoesExist);
}
return super.getMessage();
}
Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/GraphSession.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/GraphSession.java
(rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/GraphSession.java 2009-07-09
16:23:09 UTC (rev 1081)
@@ -0,0 +1,2562 @@
+/*
+ * JBoss DNA (
http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site:
http://www.fsf.org.
+ */
+package org.jboss.dna.graph.session;
+
+import java.security.AccessControlException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import net.jcip.annotations.Immutable;
+import net.jcip.annotations.NotThreadSafe;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.collection.ReadOnlyIterator;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.common.util.StringUtil;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Results;
+import org.jboss.dna.graph.Subgraph;
+import org.jboss.dna.graph.connector.RepositorySourceException;
+import org.jboss.dna.graph.connector.UuidAlreadyExistsException;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.PathNotFoundException;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.request.BatchRequestBuilder;
+import org.jboss.dna.graph.request.ChangeRequest;
+import org.jboss.dna.graph.request.CreateNodeRequest;
+import org.jboss.dna.graph.request.InvalidWorkspaceException;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.session.GraphSession.Authorizer.Action;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+
+/**
+ * This class represents an interactive session for working with the content within a
graph. This session maintains a cache of
+ * content read from the repository, as well as transient changes that have been made to
the nodes within this session that are
+ * then pushed to the graph when the session is {@link #save() saved}.
+ * <p>
+ * Like the other Graph APIs, the mutable objects in this session should not be held onto
for very long periods of time. When the
+ * session is {@link #save() saved} or {{@link #refresh(boolean) refreshed} (or when a
node is {@link #save(Node) saved} or
+ * {@link #refresh(Node, boolean) refreshed}), the session may {@link Node#unload()
unload} and discard some of its nodes. Using
+ * nodes after they are discarded may result in assertion errors (assuming Java
assertions are enabled).
+ * </p>
+ *
+ * @param <Payload> the type of the payload object for each node, used to allow the
nodes to hold additional cached information
+ * @param <PropertyPayload> the type of payload object for each property, used to
allow the nodes to hold additional cached
+ * information
+ */
+@NotThreadSafe
+public class GraphSession<Payload, PropertyPayload> {
+
+ protected final ListMultimap<Name, Node<Payload, PropertyPayload>>
NO_CHILDREN = Multimaps.immutableMultimap();
+ protected final Map<Name, PropertyInfo<PropertyPayload>> NO_PROPERTIES =
Collections.emptyMap();
+
+ protected final Authorizer authorizer;
+ protected final ExecutionContext context;
+ protected final Graph store;
+ protected final Node<Payload, PropertyPayload> root;
+ protected final Operations<Payload, PropertyPayload> nodeOperations;
+ protected final PathFactory pathFactory;
+ protected final NodeIdFactory idFactory;
+ protected final String workspaceName;
+ protected int loadDepth = 1;
+
+ /**
+ * A map of the nodes keyed by their identifier.
+ */
+ protected final Map<NodeId, Node<Payload, PropertyPayload>> nodes = new
HashMap<NodeId, Node<Payload, PropertyPayload>>();
+ /**
+ * A map that records how the changes to a node are dependent upon other nodes.
+ */
+ protected final Map<NodeId, Dependencies> changeDependencies = new
HashMap<NodeId, Dependencies>();
+
+ private LinkedList<Request> requests;
+ private BatchRequestBuilder requestBuilder;
+ protected Graph.Batch operations;
+
+ /**
+ * Create a session that uses the supplied graph and the supplied node operations.
+ *
+ * @param graph the graph that this session is to use
+ * @param workspaceName the name of the workspace that is to be used, or null if the
current workspace should be used
+ * @param nodeOperations the operations that are to be performed during various
stages in the lifecycle of a node, or null if
+ * there are no special operations that should be performed
+ */
+ public GraphSession( Graph graph,
+ String workspaceName,
+ Operations<Payload, PropertyPayload> nodeOperations ) {
+ this(graph, workspaceName, nodeOperations, null);
+ }
+
+ /**
+ * Create a session that uses the supplied graph and the supplied node operations.
+ *
+ * @param graph the graph that this session is to use
+ * @param workspaceName the name of the workspace that is to be used, or null if the
current workspace should be used
+ * @param nodeOperations the operations that are to be performed during various
stages in the lifecycle of a node, or null if
+ * there are no special operations that should be performed
+ * @param authorizer the authorizing component, or null if no special authorization
is to be performed
+ * @throws IllegalArgumentException if the graph reference is null
+ * @throws IllegalArgumentException if the depth is not positive
+ */
+ public GraphSession( Graph graph,
+ String workspaceName,
+ Operations<Payload, PropertyPayload> nodeOperations,
+ Authorizer authorizer ) {
+ assert graph != null;
+ this.store = graph;
+ this.context = store.getContext();
+ if (workspaceName != null) {
+ this.workspaceName = this.store.useWorkspace(workspaceName).getName();
+ } else {
+ this.workspaceName = this.store.getCurrentWorkspaceName();
+ }
+ this.nodeOperations = nodeOperations != null ? nodeOperations : new
SimpleNodeOperations<Payload, PropertyPayload>();
+ this.pathFactory = context.getValueFactories().getPathFactory();
+ this.authorizer = authorizer != null ? authorizer : new NoOpAuthorizer();
+ // Create the NodeId factory ...
+ this.idFactory = new NodeIdFactory() {
+ private long nextId = 0L;
+
+ public NodeId create() {
+ return new NodeId(++nextId);
+ }
+ };
+ // Create the root node ...
+ Location rootLocation = Location.create(pathFactory.createRootPath());
+ NodeId rootId = idFactory.create();
+ this.root = createNode(null, rootId, rootLocation);
+ this.nodes.put(rootId, root);
+
+ // Create the batch operations ...
+ this.requests = new LinkedList<Request>();
+ this.requestBuilder = new BatchRequestBuilder(this.requests);
+ this.operations = this.store.batch(this.requestBuilder);
+ }
+
+ /**
+ * Get the subgraph depth that is read when a node is loaded from the persistence
store. By default, this value is 1.
+ *
+ * @return the loading depth; always positive
+ */
+ public int getDepthForLoadingNodes() {
+ return loadDepth;
+ }
+
+ /**
+ * Set the loading depth parameter, which controls how deep a subgraph should be read
when a node is loaded from the
+ * persistence store. By default, this value is 1.
+ *
+ * @param depth the depth that should be read whenever a single node is loaded
+ * @throws IllegalArgumentException if the depth is not positive
+ */
+ public void setDepthForLoadingNodes( int depth ) {
+ CheckArg.isPositive(depth, "depth");
+ this.loadDepth = depth;
+ }
+
+ /**
+ * Get the root node.
+ *
+ * @return the root node; never null
+ */
+ public Node<Payload, PropertyPayload> getRoot() {
+ return root;
+ }
+
+ /**
+ * Get the path factory that should be used to adjust the path objects.
+ *
+ * @return the path factory; never null
+ */
+ public PathFactory getPathFactory() {
+ return pathFactory;
+ }
+
+ /**
+ * Find in the session the node with the supplied identifier.
+ *
+ * @param id the identifier of the node
+ * @return the identified node, or null if the session has no node with the supplied
identifier
+ * @throws IllegalArgumentException if the identifier is null
+ */
+ public Node<Payload, PropertyPayload> findNodeWith( NodeId id ) {
+ CheckArg.isNotNull(id, "id");
+ return nodes.get(id);
+ }
+
+ /**
+ * 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
+ * was found by the identifier, the resulting may not have the same path as that
supplied as a parameter.
+ *
+ * @param id the identifier to the node; may be null if the node is to be found by
path
+ * @param path the path that should be used to find the node only when the cache
doesn't contain a node with the identifier
+ * @return the node with the supplied id and/or path
+ * @throws IllegalArgumentException if the identifier and path are both node
+ * @throws PathNotFoundException if the node at the supplied path does not exist
+ * @throws AccessControlException if the user does not have permission to read the
nodes given by the supplied path
+ */
+ public Node<Payload, PropertyPayload> findNodeWith( NodeId id,
+ Path path ) throws
PathNotFoundException, AccessControlException {
+ if (id == null && path == null) {
+ CheckArg.isNotNull(path, "path and id");
+ }
+ Node<Payload, PropertyPayload> result = nodes.get(id); // if found, the
user should have read privilege since it was
+ // already in the cache
+ if (result == null && path != null) {
+ result = findNodeWith(path);
+ }
+ return result;
+ }
+
+ /**
+ * 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
+ * cache, it loads the nodes down the supplied path.
+ *
+ * @param path the path to the node
+ * @return the node information
+ * @throws PathNotFoundException if the node at the supplied path does not exist
+ * @throws AccessControlException if the user does not have permission to read the
nodes given by the supplied path
+ */
+ public Node<Payload, PropertyPayload> findNodeWith( Path path ) throws
PathNotFoundException, AccessControlException {
+ if (path.isRoot()) return getRoot();
+ return findNodeRelativeTo(root, path.relativeTo(root.getPath()));
+ }
+
+ /**
+ * Find the node with the supplied path relative to another node. This node quickly
finds the node by walking the supplied
+ * relative path starting at the supplied node. As soon as a cached node is found to
not be fully loaded, the persistent
+ * information for that node and all remaining nodes along the relative path are read
from the persistent store and inserted
+ * into the cache.
+ *
+ * @param startingPoint the node from which the path is relative
+ * @param relativePath the relative path from the designated starting point to the
desired node; may not be null and may not
+ * be an {@link Path#isAbsolute() absolute} path
+ * @return the node information
+ * @throws PathNotFoundException if the node at the supplied path does not exist
+ * @throws AccessControlException if the user does not have permission to read the
nodes given by the supplied path
+ */
+ @SuppressWarnings( "synthetic-access" )
+ public Node<Payload, PropertyPayload> findNodeRelativeTo( Node<Payload,
PropertyPayload> startingPoint,
+ Path relativePath )
+ throws PathNotFoundException, AccessControlException {
+ Node<Payload, PropertyPayload> node = startingPoint;
+ if (!relativePath.isRoot()) {
+ // Find the absolute path, which ensures that the relative path is
well-formed ...
+ Path absolutePath = relativePath.resolveAgainst(startingPoint.getPath());
+
+ // Verify that the user has the appropriate privileges to read these
nodes...
+ authorizer.checkPermissions(absolutePath, Action.READ);
+
+ // Walk down the path ...
+ Iterator<Path.Segment> iter = relativePath.iterator();
+ while (iter.hasNext()) {
+ Path.Segment segment = iter.next();
+ try {
+ if (segment.isSelfReference()) continue;
+ if (segment.isParentReference()) {
+ node = node.getParent();
+ assert node != null; // since the relative path is well-formed
+ continue;
+ }
+
+ if (node.isLoaded()) {
+ // The child is the next node we need to process ...
+ node = node.getChild(segment);
+ } else {
+ // The node has not yet been loaded into the cache, so read this
node
+ // from the store as well as all nodes along the path to the node
we're really
+ // interested in. We'll do this in a batch, so first create
this batch ...
+ Graph.Batch batch = store.batch();
+
+ // Figure out which nodes along the path need to be loaded from
the store ...
+ Path firstPath = node.getPath();
+ batch.read(firstPath);
+ // Now add the path to the child (which is no longer on the
iterator) ...
+ Path nextPath = pathFactory.create(firstPath, segment);
+ if (!iter.hasNext() && loadDepth > 1) {
+ batch.readSubgraphOfDepth(loadDepth).at(nextPath);
+ } else {
+ batch.read(nextPath);
+ }
+ // Now add any remaining paths that are still on the iterator
...
+ while (iter.hasNext()) {
+ nextPath = pathFactory.create(nextPath, iter.next());
+ if (!iter.hasNext() && loadDepth > 1) {
+ batch.readSubgraphOfDepth(loadDepth).at(nextPath);
+ } else {
+ batch.read(nextPath);
+ }
+ }
+
+ // Load all of the nodes (we should be reading at least 2 nodes)
...
+ Results batchResults = batch.execute();
+
+ // Add the children and properties in the lowest cached node ...
+ Path previousPath = null;
+ Node<Payload, PropertyPayload> topNode = node;
+ Node<Payload, PropertyPayload> previousNode = node;
+ for (org.jboss.dna.graph.Node persistentNode : batchResults) {
+ Location location = persistentNode.getLocation();
+ Path path = location.getPath();
+ if (path.isRoot()) {
+ previousNode = root;
+ root.location = location;
+ } else {
+ if (path.getParent().equals(previousPath)) {
+ previousNode =
previousNode.getChild(path.getLastSegment());
+ } else {
+ Path subgraphPath =
path.relativeTo(topNode.getPath());
+ previousNode = findNodeRelativeTo(topNode,
subgraphPath);
+ }
+ // Set the node that we're looking for ...
+ if
(path.getLastSegment().equals(relativePath.getLastSegment()) &&
path.equals(absolutePath)) {
+ node = previousNode;
+ }
+ }
+ nodeOperations.update(persistentNode, previousNode);
+ previousPath = path;
+ }
+ }
+ } catch (PathNotFoundException e) {
+ // Use the correct desired path ...
+ throw new PathNotFoundException(Location.create(relativePath),
e.getLowestAncestorThatDoesExist());
+ }
+ }
+ }
+ return node;
+ }
+
+ /**
+ * Returns whether the session cache has any pending changes that need to be
executed.
+ *
+ * @return true if there are pending changes, or false if there is currently no
changes
+ */
+ public boolean hasPendingChanges() {
+ return root.isChanged(true);
+ }
+
+ /**
+ * Remove any cached information that has been marked as a transient change.
+ */
+ public void clearAllChangedNodes() {
+ root.clearChanges();
+ changeDependencies.clear();
+ requests.clear();
+ }
+
+ /**
+ * Move this node from its current location so that is is a child of the supplied
parent, but do so immediately without
+ * enqueuing the operation within the session's operations.
+ *
+ * @param nodeToMove the node that is to be moved; may not be null
+ * @param newParent the new parent for this node; may not be null
+ * @param newName the new name for the node, or null if the node is not to be
renamed
+ * @throws RepositorySourceException if the parent node is to be loaded but a problem
is encountered while doing so
+ * @throws IllegalArgumentException if the node being moved is the root node
+ */
+ public void immediateMove( Node<Payload, PropertyPayload> nodeToMove,
+ Node<Payload, PropertyPayload> newParent,
+ Name newName ) {
+ CheckArg.isNotNull(nodeToMove, "nodeToMove");
+ CheckArg.isNotNull(newParent, "newParent");
+ nodeToMove.moveTo(newParent, newName, false);
+ }
+
+ /**
+ * Copy the node at the supplied source path in the named workspace, and place the
copy at the supplied location within the
+ * current workspace, doing so immediately without enqueuing the operation within the
session's operations. The current
+ * session is modified immediately to reflect the copy result.
+ * <p>
+ * Note that the destination path should not include a same-name-sibling index, since
this will be ignored and will always be
+ * recomputed (as the copy will be appended to any children already in the
destination's parent).
+ * </p>
+ *
+ * @param source the path to the node that is to be copied; may not be null
+ * @param destination the path where the copy is to be placed; may not be null index
+ * @throws IllegalArgumentException either path is null or invalid
+ */
+ public void immediateCopy( Path source,
+ Path destination ) {
+ immediateCopy(source, workspaceName, destination);
+ }
+
+ /**
+ * Copy the node at the supplied source path in the named workspace, and place the
copy at the supplied location within the
+ * current workspace, doing so immediately without enqueuing the operation within the
session's operations. The current
+ * session is modified immediately to reflect the copy result.
+ * <p>
+ * Note that the destination path should not include a same-name-sibling index, since
this will be ignored and will always be
+ * recomputed (as the copy will be appended to any children already in the
destination's parent).
+ * </p>
+ *
+ * @param source the path to the node that is to be copied; may not be null
+ * @param sourceWorkspace the name of the workspace where the source node is to be
found, or null if the current workspace
+ * should be used
+ * @param destination the path where the copy is to be placed; may not be null index
+ * @throws IllegalArgumentException either path is null or invalid
+ * @throws InvalidWorkspaceException if the source workspace name is invalid or does
not exist
+ */
+ public void immediateCopy( Path source,
+ String sourceWorkspace,
+ Path destination ) {
+ CheckArg.isNotNull(source, "source");
+ CheckArg.isNotNull(destination, "destination");
+ if (sourceWorkspace == null) sourceWorkspace = workspaceName;
+
+ // Perform the copy operation, but use the "to" form (not the
"into", which takes the parent), but
+ // but use a batch so that we can read the latest list of children ...
+ Results results = store.batch()
+ .copy(source)
+ .fromWorkspace(sourceWorkspace)
+ .to(destination)
+ .and()
+ .readChildren()
+ .of(destination.getParent())
+ .execute();
+
+ // Now get the children of the destination's parent ...
+ Location parentLocation = results.getNode(destination).getLocation();
+ // Find the parent node in the session ...
+ Node<Payload, PropertyPayload> parent =
this.findNodeWith(parentLocation.getPath());
+ if (parent.isLoaded()) {
+ // Update the children to make them match the latest snapshot from the store
...
+ List<Location> newChildren =
results.getNode(parentLocation).getChildren();
+ parent.synchronize(newChildren);
+ }
+ }
+
+ /**
+ * Clone the supplied source branch and place into the destination location,
optionally removing any existing copy that
+ * already exists in the destination location.
+ *
+ * @param source
+ * @param sourceWorkspace
+ * @param destination
+ * @param removeExisting true if the original should be removed, or false if the
original should be left
+ * @throws UuidAlreadyExistsException if copy could not be completed because the
current workspace already includes at least
+ * one of the nodes at or below the <code>source</code> branch in
the source workspace
+ */
+ public void immediateClone( Path source,
+ String sourceWorkspace,
+ Path destination,
+ boolean removeExisting ) {
+
+ // Perform the copy operation, but use the "to" form (not the
"into", which takes the parent), but
+ // but use a batch so that we can read the latest list of children ...
+ Graph.Batch batch = store.batch();
+ if (removeExisting) {
+ // Perform the copy operation, but use the "to" form (not the
"into", which takes the parent) ...
+
batch.copy(source).replacingExistingNodesWithSameUuids().fromWorkspace(sourceWorkspace).to(destination);
+ } else {
+ // Perform the copy operation, but use the "to" form (not the
"into", which takes the parent) ...
+
batch.copy(source).failingIfUuidsMatch().fromWorkspace(sourceWorkspace).to(destination);
+ }
+ // And read the children of the destination's parent ...
+ batch.readChildren().of(destination.getParent());
+ // Now execute these two operations ...
+ Results results = batch.execute();
+
+ // Now get the children of the destination's parent ...
+ Location parentLocation = results.getNode(destination).getLocation();
+ // Find the parent node in the session ...
+ Node<Payload, PropertyPayload> parent =
this.findNodeWith(parentLocation.getPath());
+ if (parent.isLoaded()) {
+ // Update the children to make them match the latest snapshot from the store
...
+ List<Location> newChildren =
results.getNode(parentLocation).getChildren();
+ parent.synchronize(newChildren);
+ }
+ }
+
+ /**
+ * Refreshes (removes the cached state) for all cached nodes.
+ * <p>
+ * If {@code keepChanges == true}, modified nodes will not have their state
refreshed, while all others will either be
+ * unloaded or changed to reflect the current state of the persistent store.
+ * </p>
+ *
+ * @param keepChanges indicates whether changed nodes should be kept or refreshed
from the repository.
+ * @throws InvalidStateException if any error resulting while reading information
from the repository
+ * @throws RepositorySourceException if any error resulting while reading information
from the repository
+ */
+ @SuppressWarnings( "synthetic-access" )
+ public void refresh( boolean keepChanges ) throws InvalidStateException,
RepositorySourceException {
+ if (keepChanges) {
+ refresh(root, keepChanges);
+ } else {
+ // Clear out all state ...
+ nodes.clear();
+ nodes.put(root.getNodeId(), root);
+ // Clear out all changes ...
+ requests.clear();
+ changeDependencies.clear();
+ // And force the root node to be 'unloaded' (in an efficient way)
...
+ root.changed = false;
+ root.childrenByName = null;
+ root.expirationTime = Long.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Refreshes (removes the cached state) for the given node and its descendants.
+ * <p>
+ * If {@code keepChanges == true}, modified nodes will not have their state
refreshed, while all others will either be
+ * unloaded or changed to reflect the current state of the persistent store.
+ * </p>
+ *
+ * @param node the node that is to be refreshed; may not be null
+ * @param keepChanges indicates whether changed nodes should be kept or refreshed
from the repository.
+ * @throws InvalidStateException if any error resulting while reading information
from the repository
+ * @throws RepositorySourceException if any error resulting while reading information
from the repository
+ */
+ public void refresh( Node<Payload, PropertyPayload> node,
+ boolean keepChanges ) throws InvalidStateException,
RepositorySourceException {
+ if (!node.isRoot() && node.isChanged(true)) {
+ // Need to make sure that changes to this branch are not dependent upon
changes to nodes outside of this branch...
+ if (!node.containsChangesWithExternalDependencies()) {
+ I18n msg =
GraphI18n.unableToRefreshBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
+ String path = node.getPath().getString(context.getNamespaceRegistry());
+ throw new InvalidStateException(msg.text(path, workspaceName));
+ }
+ }
+
+ if (keepChanges && node.isChanged(true)) {
+ // Perform the refresh while retaining changes ...
+ // Phase 1: determine which nodes can be unloaded, which must be refreshed,
and which must be unchanged ...
+ RefreshState<Payload, PropertyPayload> refreshState = new
RefreshState<Payload, PropertyPayload>();
+ node.refreshPhase1(refreshState);
+ // If there are any nodes to be refreshed, read then in a single batch ...
+ Results readResults = null;
+ if (!refreshState.getNodesToBeRefreshed().isEmpty()) {
+ Graph.Batch batch = store.batch();
+ for (Node<Payload, PropertyPayload> nodeToBeRefreshed :
refreshState.getNodesToBeRefreshed()) {
+ batch.read(nodeToBeRefreshed.getLocation());
+ }
+ // Execute the reads. No modifications have been made to the cache, so it
is not a problem
+ // if this throws a repository exception.
+ readResults = batch.execute();
+ }
+
+ // Phase 2: update the cache by unloading or refreshing the nodes ...
+ node.refreshPhase2(refreshState, readResults);
+ } else {
+ // Get rid of all changes ...
+ node.clearChanges();
+ // And then unload the node ...
+ node.unload();
+
+ // Throw out the old pending operations
+ if (operations.isExecuteRequired()) {
+ // Make sure the builder has finished all the requests ...
+ this.requestBuilder.finishPendingRequest();
+
+ // Remove all of the enqueued requests for this branch ...
+ for (Iterator<Request> iter = this.requests.iterator();
iter.hasNext();) {
+ Request request = iter.next();
+ assert request instanceof ChangeRequest;
+ ChangeRequest change = (ChangeRequest)request;
+ if (change.changes(workspaceName, node.getPath())) {
+ iter.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Save any changes that have been accumulated by this session.
+ *
+ * @throws PathNotFoundException if the state of this session is invalid and is
attempting to change a node that doesn't exist
+ * @throws ValidationException if any of the changes being made result in an invalid
node state
+ * @throws InvalidStateException if the supplied node is no longer a node within this
cache (because it was unloaded)
+ */
+ public void save() throws PathNotFoundException, ValidationException,
InvalidStateException {
+ if (!operations.isExecuteRequired()) return;
+ assert root.isChanged(true);
+
+ // Make sure that each of the changed node is valid. This process requires that
all children of
+ // all changed nodes are loaded, so in this process load all unloaded children in
one batch ...
+ root.onChangedNodes(new LoadAllChildrenVisitor() {
+ @Override
+ protected void finishParentAfterLoading( Node<Payload, PropertyPayload>
node ) {
+ nodeOperations.validate(node);
+ }
+ });
+
+ // Execute the batched operations ...
+ operations.execute();
+
+ // Create a new batch for future operations ...
+ // LinkedList<Request> oldRequests = this.requests;
+ this.requests = new LinkedList<Request>();
+ this.requestBuilder = new BatchRequestBuilder(this.requests);
+ operations = store.batch(this.requestBuilder);
+
+ // Remove all the cached items that have been changed or deleted ...
+ this.root.clearChanges();
+
+ // Remove all the changed and deleted infos ...
+ changeDependencies.clear();
+ }
+
+ /**
+ * Save any changes to the identified node or its descendants. The supplied node may
not have been deleted or created in this
+ * session since the last save operation.
+ *
+ * @param node the node being saved; may not be null
+ * @throws PathNotFoundException if the state of this session is invalid and is
attempting to change a node that doesn't exist
+ * @throws ValidationException if any of the changes being made result in an invalid
node state
+ * @throws InvalidStateException if the supplied node is no longer a node within this
cache (because it was unloaded)
+ */
+ public void save( Node<Payload, PropertyPayload> node )
+ throws PathNotFoundException, ValidationException, InvalidStateException {
+ assert node != null;
+ if (node.isRoot()) {
+ // We're actually saving the root, so the other 'save' method is
faster and more efficient ...
+ save();
+ return;
+ }
+ if (node.isStale()) {
+ // This node was deleted in this session ...
+ String readableLocation =
node.getLocation().getString(context.getNamespaceRegistry());
+ I18n msg = GraphI18n.nodeHasAlreadyBeenRemovedFromThisSession;
+ throw new InvalidStateException(msg.text(readableLocation, workspaceName));
+ }
+ if (!node.isChanged(true)) {
+ // There are no changes within this branch
+ return;
+ }
+
+ // Need to make sure that changes to this branch are not dependent upon changes
to nodes outside of this branch...
+ if (!node.containsChangesWithExternalDependencies()) {
+ I18n msg =
GraphI18n.unableToSaveBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
+ String path = node.getPath().getString(context.getNamespaceRegistry());
+ throw new InvalidStateException(msg.text(path, workspaceName));
+ }
+
+ // Make sure that each of the changed node is valid. This process requires that
all children of
+ // all changed nodes are loaded, so in this process load all unloaded children in
one batch ...
+ root.onChangedNodes(new LoadAllChildrenVisitor() {
+ @Override
+ protected void finishParentAfterLoading( Node<Payload, PropertyPayload>
node ) {
+ nodeOperations.validate(node);
+ }
+ });
+
+ // Make sure the builder has finished all the requests ...
+ this.requestBuilder.finishPendingRequest();
+
+ // Remove all of the enqueued requests for this branch ...
+ Path path = node.getPath();
+ LinkedList<Request> branchRequests = new LinkedList<Request>();
+ LinkedList<Request> nonBranchRequests = new LinkedList<Request>();
+ Set<UUID> branchUuids = new HashSet<UUID>();
+ for (Request request : this.requests) {
+ assert request instanceof ChangeRequest;
+ ChangeRequest change = (ChangeRequest)request;
+ if (change.changes(workspaceName, path)) {
+ branchRequests.add(request);
+ // Record the UUID of the node being saved now ...
+ UUID changedUuid = null;
+ if (change instanceof CreateNodeRequest) {
+ // We want the parent UUID ...
+ changedUuid = ((CreateNodeRequest)change).under().getUuid();
+ } else {
+ changedUuid = change.changedLocation().getUuid();
+ }
+ assert changedUuid != null;
+ branchUuids.add(changedUuid);
+ } else {
+ nonBranchRequests.add(request);
+ }
+ }
+ if (branchRequests.isEmpty()) return;
+
+ // Now execute the branch ...
+ Graph.Batch branchBatch = store.batch(new BatchRequestBuilder(branchRequests));
+ branchBatch.execute();
+
+ // Still have non-branch related requests that we haven't executed ...
+ this.requests = nonBranchRequests;
+ this.requestBuilder = new BatchRequestBuilder(this.requests);
+ this.operations = store.batch(this.requestBuilder);
+
+ // Remove all the cached, changed or deleted items that were just saved ...
+ node.clearChanges();
+ }
+
+ protected Node<Payload, PropertyPayload> createNode( Node<Payload,
PropertyPayload> parent,
+ NodeId nodeId,
+ Location location ) {
+ return new Node<Payload, PropertyPayload>(this, parent, nodeId, location);
+ }
+
+ protected long getCurrentTime() {
+ return System.currentTimeMillis();
+ }
+
+ protected void recordMove( Node<Payload, PropertyPayload> nodeBeingMoved,
+ Node<Payload, PropertyPayload> oldParent,
+ Node<Payload, PropertyPayload> newParent ) {
+ // Fix the cache's state ...
+ NodeId id = nodeBeingMoved.getNodeId();
+ Dependencies dependencies = changeDependencies.get(id);
+ if (dependencies == null) {
+ dependencies = new Dependencies();
+ dependencies.setMovedFrom(oldParent.getNodeId());
+ changeDependencies.put(id, dependencies);
+ } else {
+ dependencies.setMovedFrom(newParent.getNodeId());
+ }
+ }
+
+ /**
+ * Record the fact that the supplied node is in the process of being deleted, so any
cached information (outside of the node
+ * object itself) should be cleaned up.
+ *
+ * @param node the node being deleted; never null
+ */
+ protected void recordDelete( Node<Payload, PropertyPayload> node ) {
+ // Record the operation ...
+ operations.delete(node.getLocation());
+ // Fix the cache's state ...
+ nodes.remove(node.getNodeId());
+ changeDependencies.remove(node.getNodeId());
+ recordUnloaded(node);
+ }
+
+ /**
+ * Record the fact that the supplied node is in the process of being unloaded, so any
cached information (outside of the node
+ * object itself) should be cleaned up.
+ *
+ * @param node the node being unloaded; never null
+ */
+ protected void recordUnloaded( final Node<Payload, PropertyPayload> node ) {
+ if (node.isLoaded() && node.getChildrenCount() > 0) {
+ // Walk the branch and remove all nodes from the map of all nodes ...
+ node.onLoadedNodes(new NodeVisitor<Payload, PropertyPayload>() {
+ @SuppressWarnings( "synthetic-access" )
+ @Override
+ public boolean visit( Node<Payload, PropertyPayload> unloaded ) {
+ if (unloaded != node) { // info for 'node' should not be
removed
+ nodes.remove(unloaded.getNodeId());
+ changeDependencies.remove(unloaded.getNodeId());
+ unloaded.parent = null;
+ }
+ return true;
+ }
+ });
+ }
+ }
+
+ @ThreadSafe
+ public static interface Operations<NodePayload, PropertyPayload> {
+
+ /**
+ * Update the node with the information from the persistent store.
+ *
+ * @param persistentNode the persistent node that should be converted into a node
info; never null
+ * @param node the session's node representation that is to be updated; never
null
+ */
+ void update( org.jboss.dna.graph.Node persistentNode,
+ Node<NodePayload, PropertyPayload> node );
+
+ /**
+ * Signal that the node's {@link GraphSession.Node#getLocation() location}
has been changed
+ *
+ * @param node the node with the new location
+ * @param oldLocation the old location of the node
+ */
+ void updateLocation( Node<NodePayload, PropertyPayload> node,
+ Location oldLocation );
+
+ /**
+ * Validate a node for consistency and well-formedness.
+ *
+ * @param node the node to be validated
+ * @throws ValidationException if there is a problem during validation
+ */
+ void validate( Node<NodePayload, PropertyPayload> node ) throws
ValidationException;
+ }
+
+ @ThreadSafe
+ public static interface NodeIdFactory {
+ NodeId create();
+ }
+
+ @ThreadSafe
+ public static interface Authorizer {
+
+ public enum Action {
+ READ,
+ REMOVE,
+ ADD_NODE,
+ SET_PROPERTY;
+ }
+
+ /**
+ * Throws an {@link AccessControlException} if the current user is not able to
perform the action on the node at the
+ * supplied path in the current workspace.
+ *
+ * @param path the path on which the actions are occurring
+ * @param action the action to check
+ * @throws AccessControlException if the user does not have permission to perform
the actions
+ */
+ void checkPermissions( Path path,
+ Action action ) throws AccessControlException;
+ }
+
+ /**
+ * {@link Authorizer} implementation that does nothing.
+ */
+ @ThreadSafe
+ protected static class NoOpAuthorizer implements Authorizer {
+ /**
+ * {@inheritDoc}
+ *
+ * @see
org.jboss.dna.graph.session.GraphSession.Authorizer#checkPermissions(org.jboss.dna.graph.property.Path,
+ * org.jboss.dna.graph.session.GraphSession.Authorizer.Action)
+ */
+ public void checkPermissions( Path path,
+ Action action ) throws AccessControlException {
+ }
+ }
+
+ @ThreadSafe
+ protected static class SimpleNodeOperations<Payload, PropertyPayload>
implements Operations<Payload, PropertyPayload> {
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.Operations#update(org.jboss.dna.graph.Node,
GraphSession.Node)
+ */
+ public void update( org.jboss.dna.graph.Node persistentNode,
+ Node<Payload, PropertyPayload> node ) {
+ // Create the map of property info objects ...
+ Map<Name, PropertyInfo<PropertyPayload>> properties = new
HashMap<Name, PropertyInfo<PropertyPayload>>();
+ for (Property property : persistentNode.getProperties()) {
+ Name propertyName = property.getName();
+ PropertyId id = new PropertyId(node.getNodeId(), propertyName);
+ PropertyInfo<PropertyPayload> info = new
PropertyInfo<PropertyPayload>(id, property, property.isMultiple(),
+
PropertyStatus.UNCHANGED, null);
+ properties.put(propertyName, info);
+ }
+ // Set only the children ...
+ node.loadedWith(persistentNode.getChildren(), properties,
persistentNode.getExpirationTime());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.Operations#updateLocation(GraphSession.Node,
org.jboss.dna.graph.Location)
+ */
+ public void updateLocation( Node<Payload, PropertyPayload> node,
+ Location oldLocation ) {
+ // do nothing here
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.Operations#validate(GraphSession.Node)
+ */
+ public void validate( Node<Payload, PropertyPayload> node ) {
+ // do nothing here
+ }
+ }
+
+ @NotThreadSafe
+ public static class Node<Payload, PropertyPayload> {
+ private final GraphSession<Payload, PropertyPayload> cache;
+ private final NodeId nodeId;
+ private Node<Payload, PropertyPayload> parent;
+ private long expirationTime = Long.MAX_VALUE;
+ private Location location;
+ private boolean changed;
+ private boolean changedBelow;
+ private Map<Name, PropertyInfo<PropertyPayload>> properties;
+ private ListMultimap<Name, Node<Payload, PropertyPayload>>
childrenByName;
+ private Payload payload;
+
+ protected Node( GraphSession<Payload, PropertyPayload> cache,
+ Node<Payload, PropertyPayload> parent,
+ NodeId nodeId,
+ Location location ) {
+ this.cache = cache;
+ this.parent = parent;
+ this.nodeId = nodeId;
+ this.location = location;
+ assert this.cache != null;
+ assert this.nodeId != null;
+ assert this.location != null;
+ assert this.location.hasPath();
+ }
+
+ /**
+ * Get the time when this node expires.
+ *
+ * @return the time in milliseconds past the epoch when this node's cached
information expires, or {@link Long#MAX_VALUE
+ * Long.MAX_VALUE} if there is no expiration or if the node has not been
loaded
+ * @see #isExpired()
+ * @see #isLoaded()
+ */
+ public final long getExpirationTimeInMillis() {
+ assert !isStale();
+ return expirationTime;
+ }
+
+ /**
+ * Determine if this node's information has expired. This method will never
return true if the node is not loaded. This
+ * method is idempotent.
+ *
+ * @return true if this node's information has been read from the store and
is expired
+ */
+ public final boolean isExpired() {
+ return expirationTime != Long.MAX_VALUE && expirationTime <
cache.getCurrentTime();
+ }
+
+ /**
+ * Determine if this node is loaded and usable. Even though the node may have
been loaded previously, this method may
+ * return false (and unloads the cached information) if the cached information
has expired and thus is no longer usable.
+ * Note, however, that changes on or below this node will prevent the node from
being unloaded.
+ *
+ * @return true if the node's information has already been loaded and may be
used, or false otherwise
+ */
+ public final boolean isLoaded() {
+ if (childrenByName == null) return false;
+ // Otherwise, it is already loaded. First see if this is expired ...
+ if (isExpired()) {
+ // It is expired, so we'd normally return false. But we should not
unload if it has changes ...
+ if (isChanged(true)) return true;
+ // It is expired and contains no changes on this branch, so we can unload
it ...
+ unload();
+ return false;
+ }
+ // Otherwise it is loaded and not expired ...
+ return true;
+ }
+
+ /**
+ * Method that causes the information for this node to be read from the store and
loaded into the cache
+ *
+ * @throws RepositorySourceException if there is a problem reading the store
+ */
+ protected final void load() throws RepositorySourceException {
+ if (isLoaded()) return;
+ assert !isStale();
+ // Check authorization before reading ...
+ Path path = getPath();
+ cache.authorizer.checkPermissions(path, Action.READ);
+ int depth = cache.getDepthForLoadingNodes();
+ if (depth == 1) {
+ // Then read the node from the store ...
+ org.jboss.dna.graph.Node persistentNode =
cache.store.getNodeAt(getLocation());
+ // Check the actual location ...
+ Location actualLocation = persistentNode.getLocation();
+ if (!this.location.equals(actualLocation)) {
+ // The actual location is changed, so update it ...
+ this.location = actualLocation;
+ }
+ // Update the persistent information ...
+ cache.nodeOperations.update(persistentNode, this);
+ } else {
+ // Then read the node from the store ...
+ Subgraph subgraph =
cache.store.getSubgraphOfDepth(depth).at(getLocation());
+ Location actualLocation = subgraph.getLocation();
+ if (!this.location.equals(actualLocation)) {
+ // The actual location is changed, so update it ...
+ this.location = actualLocation;
+ }
+ // Update the persistent information ...
+ cache.nodeOperations.update(subgraph.getRoot(), this);
+ // Now update any nodes below this node ...
+ for (org.jboss.dna.graph.Node persistentNode : subgraph) {
+ // Find the node at the path ...
+ Path relativePath =
persistentNode.getLocation().getPath().relativeTo(path);
+ Node<Payload, PropertyPayload> node =
cache.findNodeRelativeTo(this, relativePath);
+ if (!node.isLoaded()) {
+ // Update the persistent information ...
+ cache.nodeOperations.update(persistentNode, node);
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility method to unload this cached node.
+ */
+ protected final void unload() {
+ assert !isStale();
+ assert !changed;
+ assert !changedBelow;
+ if (!isLoaded()) return;
+ cache.recordUnloaded(this);
+ childrenByName = null;
+ expirationTime = Long.MAX_VALUE;
+ }
+
+ /**
+ * Phase 1 of the process of refreshing the cached content while retaining
changes. This phase walks the entire tree to
+ * determine which nodes have changes, which nodes can be unloaded, and which
nodes have no changes but are ancestors of
+ * those nodes with changes (and therefore have to be refreshed). Each node has a
{@link #isChanged(boolean) changed
+ * state}, and the supplied RefreshState tracks which nodes must be
+ * {@link GraphSession.RefreshState#markAsRequiringRefresh(Node) refreshed} in
+ * {@link #refreshPhase2(RefreshState, Results) phase 2}; all other nodes are
able to be unloaded in
+ * {@link #refreshPhase2(RefreshState, Results) phase 2}.
+ *
+ * @param refreshState the holder of the information about which nodes are to be
unloaded or refreshed; may not be null
+ * @return true if the node could be (or already is) unloaded, or false
otherwise
+ * @see #refreshPhase2(RefreshState, Results)
+ */
+ protected final boolean refreshPhase1( RefreshState<Payload,
PropertyPayload> refreshState ) {
+ assert !isStale();
+ if (childrenByName == null) {
+ // This node is not yet loaded, so don't record it as needing to be
unloaded but return true
+ return true;
+ }
+ // Perform phase 1 on each of the children ...
+ boolean canUnloadChildren = true;
+ for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
+ if (child.refreshPhase1(refreshState)) {
+ // The child can be unloaded
+ canUnloadChildren = false;
+ }
+ }
+
+ // If this node has changes, then we cannot do anything with this node ...
+ if (isChanged(false)) return false;
+
+ // Otherwise, this node contains no changes ...
+ if (canUnloadChildren) {
+ // Since all the children can be unloaded, we can completely unload this
node ...
+ return true;
+ }
+ // Otherwise, we have to hold onto the children, so we can't unload and
must be refreshed ...
+ refreshState.markAsRequiringRefresh(this);
+ return false;
+ }
+
+ /**
+ * Phase 2 of the process of refreshing the cached content while retaining
changes. This phase walks the graph and either
+ * unloads the node or, if the node is an ancestor of changed nodes, refreshes
the node state to reflect that of the
+ * persistent store.
+ *
+ * @param refreshState
+ * @param persistentInfoForRefreshedNodes
+ * @see #refreshPhase1(RefreshState)
+ */
+ protected final void refreshPhase2( RefreshState<Payload, PropertyPayload>
refreshState,
+ Results persistentInfoForRefreshedNodes ) {
+ assert !isStale();
+ if (this.changed) {
+ // There's nothing to do ...
+ return;
+ }
+ if (refreshState.requiresRefresh(this)) {
+ // This node must be refreshed since it has no changes but is an ancestor
of a node that is changed.
+ // Therefore, update the children and properties with the just-read
persistent information ...
+ assert childrenByName != null;
+ org.jboss.dna.graph.Node persistentNode =
persistentInfoForRefreshedNodes.getNode(location);
+ assert !persistentNode.getChildren().isEmpty();
+ // Find the map of old Node objects keyed by their identifier ...
+ Map<NodeId, Node<Payload, PropertyPayload>> oldChildren = new
HashMap<NodeId, Node<Payload, PropertyPayload>>();
+ for (Node<Payload, PropertyPayload> oldChild :
childrenByName.values()) {
+ oldChildren.put(oldChild.getNodeId(), oldChild);
+ }
+ childrenByName.clear();
+ for (Location location : persistentNode.getChildren()) {
+ Name childName = location.getPath().getLastSegment().getName();
+ NodeId nodeId = cache.idFactory.create();
+ Node<Payload, PropertyPayload> child =
oldChildren.remove(nodeId);
+ if (child == null) {
+ child = cache.createNode(this, nodeId, location);
+ cache.nodes.put(child.getNodeId(), child);
+ assert child.getName().equals(childName);
+ }
+ assert child.parent == this;
+ List<Node<Payload, PropertyPayload>> currentChildren =
childrenByName.get(childName);
+ currentChildren.add(child);
+ // Create a segment with the SNS ...
+ Path.Segment segment = cache.pathFactory.createSegment(childName,
currentChildren.size());
+ child.updateLocation(segment);
+ // TODO: Can the location be different? If so, doesn't that mean
that the change requests
+ // have to be updated???
+ }
+ return;
+ }
+ // This node can be unloaded (since it has no changes and isn't above a
node with changes) ...
+ unload();
+ }
+
+ /**
+ * Define the persistent child information that this node is to be populated
with. This method does not cause the node's
+ * information to be read from the store.
+ * <p>
+ * This method is intended to be called by the {@link
GraphSession.Operations#update(org.jboss.dna.graph.Node, Node)}, and
+ * should not be called by other components.
+ * </p>
+ *
+ * @param children the children for this node; may not be null
+ * @param properties the properties for this node; may not be null
+ * @param expirationTime the time that this cached information expires, or null
if there is no expiration
+ */
+ public void loadedWith( List<Location> children,
+ Map<Name, PropertyInfo<PropertyPayload>>
properties,
+ DateTime expirationTime ) {
+ assert !isStale();
+ // Load the children ...
+ if (children.isEmpty()) {
+ childrenByName = cache.NO_CHILDREN;
+ } else {
+ childrenByName = Multimaps.newLinkedListMultimap();
+ for (Location location : children) {
+ NodeId id = cache.idFactory.create();
+ Name childName = location.getPath().getLastSegment().getName();
+ Node<Payload, PropertyPayload> child = cache.createNode(this,
id, location);
+ cache.nodes.put(child.getNodeId(), child);
+ List<Node<Payload, PropertyPayload>> currentChildren =
childrenByName.get(childName);
+ currentChildren.add(child);
+ child.parent = this;
+ // Create a segment with the SNS ...
+ Path.Segment segment = cache.pathFactory.createSegment(childName,
currentChildren.size());
+ child.updateLocation(segment);
+ }
+ }
+ // Load the properties ...
+ if (properties.isEmpty()) {
+ this.properties = cache.NO_PROPERTIES;
+ } else {
+ this.properties = new HashMap<Name,
PropertyInfo<PropertyPayload>>(properties);
+ }
+
+ // Set the expiration time ...
+ this.expirationTime = expirationTime != null ?
expirationTime.getMilliseconds() : Long.MAX_VALUE;
+ }
+
+ /**
+ * Reconstruct the location object for this node, given the information at the
parent.
+ *
+ * @param segment the path segment for this node; may be null only when this node
is the root node
+ */
+ protected void updateLocation( Path.Segment segment ) {
+ assert !isStale();
+ Path newPath = null;
+ if (segment != null) {
+ // Recompute the path based upon the parent path ...
+ Path parentPath = getParent().getPath();
+ newPath = cache.pathFactory.create(parentPath, segment);
+ } else {
+ // This must be the root ...
+ newPath = cache.pathFactory.createRootPath();
+ assert this.isRoot();
+ }
+ Location newLocation = this.location.with(newPath);
+ if (newLocation != this.location) {
+ Location oldLocation = this.location;
+ this.location = newLocation;
+ cache.nodeOperations.updateLocation(this, oldLocation);
+ }
+
+ if (isLoaded() && childrenByName != cache.NO_CHILDREN) {
+ // Update all of the children ...
+ for (Map.Entry<Name, Collection<Node<Payload,
PropertyPayload>>> entry : childrenByName.asMap().entrySet()) {
+ Name childName = entry.getKey();
+ int sns = 1;
+ for (Node<Payload, PropertyPayload> child : entry.getValue())
{
+ Path.Segment childSegment =
cache.pathFactory.createSegment(childName, sns++);
+ child.updateLocation(childSegment);
+ }
+ }
+ }
+ }
+
+ protected void synchronize( List<Location> actualChildren ) {
+ if (!isLoaded()) return;
+ for (Location actualChild : actualChildren) {
+ Path.Segment actualSegment = actualChild.getPath().getLastSegment();
+ Node<Payload, PropertyPayload> existingChild =
parent.getChild(actualSegment);
+ if (existingChild == null) {
+ // The child doesn't exist yet ...
+ NodeId nodeId = cache.idFactory.create();
+ Node<Payload, PropertyPayload> newChild =
cache.createNode(this, nodeId, actualChild);
+ parent.childrenByName.put(actualSegment.getName(), newChild);
+ }
+ }
+
+ }
+
+ /**
+ * Determine whether this node has been marked as having changes.
+ *
+ * @param recursive true if the nodes under this node should be checked, or false
if only this node should be checked
+ * @return true if there are changes in the specified scope, or false otherwise
+ */
+ public final boolean isChanged( boolean recursive ) {
+ assert !isStale();
+ return recursive ? this.changed || this.changedBelow : this.changed;
+ }
+
+ /**
+ * This method determines whether this node, or any nodes below it, contain
changes that depend on nodes that are outside
+ * of this branch.
+ *
+ * @return true if this branch has nodes with changes dependent on nodes outside
of this branch
+ */
+ public boolean containsChangesWithExternalDependencies() {
+ assert !isStale();
+ if (!isChanged(true)) {
+ // There are no changes in this branch ...
+ return false;
+ }
+ // Need to make sure that nodes were not moved into or out of this branch,
since that would mean that we
+ // cannot refresh this branch without also refreshing the other affected
branches (per the JCR specification) ...
+ for (Map.Entry<NodeId, Dependencies> entry :
cache.changeDependencies.entrySet()) {
+ Dependencies dependency = entry.getValue();
+ NodeId nodeId = entry.getKey();
+ Node<Payload, PropertyPayload> changedNode =
cache.nodes.get(nodeId);
+
+ // First, check whether the changed node is within the branch ...
+ if (!changedNode.isAtOrBelow(this)) {
+ // The node is not within this branch, so the original parent must
not be at or below this node ...
+ if (cache.nodes.get(dependency.getMovedFrom()).isAtOrBelow(this)) {
+ return false;
+ }
+ // None of the other dependencies can be within this branch ...
+ for (NodeId dependentId : dependency.getRequireChangesTo()) {
+ // The dependent node must not be at or below this node ...
+ if (cache.nodes.get(dependentId).isAtOrBelow(this)) {
+ return false;
+ }
+ }
+ // Otherwise, continue with the next change ...
+ continue;
+ }
+ // The changed node is within this branch!
+
+ // Second, check whether this node was moved from outside this branch
...
+ if (dependency.getMovedFrom() != null) {
+ Node<Payload, PropertyPayload> originalParent =
cache.nodes.get(dependency.getMovedFrom());
+ // The original parent must be at or below this node ...
+ if (!originalParent.isAtOrBelow(this)) {
+ return false;
+ }
+ // All of the other dependencies must be within this branch ...
+ for (NodeId dependentId : dependency.getRequireChangesTo()) {
+ // The dependent node must not be at or below this node ...
+ if (!cache.nodes.get(dependentId).isAtOrBelow(this)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Clear any transient changes that have been accumulated in this node.
+ *
+ * @see #markAsChanged()
+ */
+ public void clearChanges() {
+ assert !isStale();
+ if (this.changed) {
+ this.changed = false;
+ this.changedBelow = false;
+ unload();
+ } else {
+ if (!this.changedBelow) return;
+ // This node has not changed but something below has, so call to the
children ...
+ if (childrenByName != null && childrenByName !=
cache.NO_CHILDREN) {
+ for (Node<Payload, PropertyPayload> child :
childrenByName.values()) {
+ child.clearChanges();
+ }
+ }
+ this.changedBelow = false;
+ }
+ // Update the parent ...
+ if (this.parent != null) this.parent.recomputeChangedBelow();
+ }
+
+ /**
+ * Mark this node as having changes.
+ *
+ * @see #clearChanges()
+ */
+ public final void markAsChanged() {
+ assert !isStale();
+ this.changed = true;
+ if (this.parent != null) this.parent.markAsChangedBelow();
+ }
+
+ protected final void markAsChangedBelow() {
+ if (!this.changedBelow) {
+ this.changedBelow = true;
+ if (this.parent != null) this.parent.markAsChangedBelow();
+ }
+ }
+
+ protected final void recomputeChangedBelow() {
+ if (!this.changedBelow) return; // we're done
+ // there are changes ...
+ assert childrenByName != null;
+ for (Node<Payload, PropertyPayload> child : childrenByName.values()) {
+ if (child.isChanged(true)) {
+ this.markAsChangedBelow();
+ return;
+ }
+ }
+ // No changes found ...
+ this.changedBelow = false;
+ if (this.parent != null) this.parent.recomputeChangedBelow();
+ }
+
+ /**
+ * Move this node from its current location so that is is a child of the supplied
parent.
+ *
+ * @param parent the new parent for this node; may not be null
+ * @throws RepositorySourceException if the parent node is to be loaded but a
problem is encountered while doing so
+ * @throws IllegalArgumentException if this is the root node
+ */
+ public void moveTo( Node<Payload, PropertyPayload> parent ) {
+ moveTo(parent, null, true);
+ }
+
+ /**
+ * Move this node from its current location so that is is a child of the supplied
parent, renaming the node in the
+ * process.
+ *
+ * @param parent the new parent for this node; may not be null
+ * @param newNodeName the new name for the node, or null if the node should keep
the same name
+ * @throws RepositorySourceException if the parent node is to be loaded but a
problem is encountered while doing so
+ * @throws IllegalArgumentException if this is the root node
+ */
+ public void moveTo( Node<Payload, PropertyPayload> parent,
+ Name newNodeName ) {
+ moveTo(parent, newNodeName, true);
+ }
+
+ /**
+ * Move this node from its current location so that is is a child of the supplied
parent.
+ *
+ * @param parent the new parent for this node; may not be null
+ * @param newNodeName the new name for the node, or null if the node should keep
the same name
+ * @param useBatch true if this operation should be performed using the
session's current batch operation and executed
+ * upon {@link GraphSession#save()}, or false if the move should be
performed immediately
+ * @throws RepositorySourceException if the parent node is to be loaded but a
problem is encountered while doing so
+ * @throws IllegalArgumentException if this is the root node
+ */
+ protected void moveTo( Node<Payload, PropertyPayload> parent,
+ Name newNodeName,
+ boolean useBatch ) {
+ final Node<Payload, PropertyPayload> child = this;
+ assert !parent.isStale();
+ assert child.parent != this;
+ assert !child.isRoot();
+ if (newNodeName == null) newNodeName = getName();
+
+ // Check authorization ...
+ cache.authorizer.checkPermissions(parent.getPath(), Action.ADD_NODE);
+ cache.authorizer.checkPermissions(child.getPath().getParent(),
Action.REMOVE);
+
+ parent.load();
+ if (parent.childrenByName == cache.NO_CHILDREN) {
+ parent.childrenByName = Multimaps.newLinkedListMultimap();
+ }
+ Name childName = child.getName();
+ List<Node<Payload, PropertyPayload>> currentChildren =
parent.childrenByName.get(childName);
+ currentChildren.add(child);
+ parent.markAsChanged();
+ // Remove the child from it's existing parent ...
+ final Node<Payload, PropertyPayload> oldParent = child.parent;
+ // Record the operation ...
+ if (useBatch) {
+ if (newNodeName.equals(getName())) {
+
cache.operations.move(child.getLocation()).into(parent.getLocation());
+ } else {
+
cache.operations.move(child.getLocation()).as(newNodeName).into(parent.getLocation());
+ }
+ } else {
+ if (newNodeName.equals(getName())) {
+ cache.store.move(child.getLocation()).into(parent.getLocation());
+ } else {
+
cache.store.move(child.getLocation()).as(newNodeName).into(parent.getLocation());
+ }
+ }
+ cache.recordMove(child, oldParent, parent);
+ child.remove();
+ // Set the new parent to this node ...
+ child.parent = parent;
+ // Update the new child with the correct location ...
+ Path.Segment segment = cache.pathFactory.createSegment(childName,
currentChildren.size());
+ child.updateLocation(segment);
+ }
+
+ /**
+ * Copy this node (and all nodes below it) and place the copy under the supplied
parent location. The new copy will be
+ * appended to any existing children of the supplied parent node, and will be
given the appropriate same-name-sibling
+ * index.
+ *
+ * @param parent the new parent for the new copy; may not be null
+ * @throws RepositorySourceException if the parent node is to be loaded but a
problem is encountered while doing so
+ * @throws IllegalArgumentException if this is the root node
+ */
+ public void copyTo( Node<Payload, PropertyPayload> parent ) {
+ final Node<Payload, PropertyPayload> child = this;
+ assert !parent.isStale();
+ assert child.parent != this;
+ assert !child.isRoot();
+
+ // Check authorization ...
+ cache.authorizer.checkPermissions(parent.getPath(), Action.ADD_NODE);
+ cache.authorizer.checkPermissions(child.getPath(), Action.READ);
+
+ parent.load();
+ if (parent.childrenByName == cache.NO_CHILDREN) {
+ parent.childrenByName = Multimaps.newLinkedListMultimap();
+ }
+ Name childName = child.getName();
+ // Figure out the name and SNS of the new copy ...
+ List<Node<Payload, PropertyPayload>> currentChildren =
parent.childrenByName.get(childName);
+ Location copyLocation =
Location.create(cache.pathFactory.create(parent.getPath(), childName,
currentChildren.size()));
+
+ // Perform the copy ...
+ cache.operations.copy(child.getLocation()).to(copyLocation);
+
+ // Add the child to the parent ...
+ cache.createNode(parent, cache.idFactory.create(), copyLocation);
+ parent.markAsChanged();
+ }
+
+ /**
+ * Clone this node (and all nodes below it). The new copy will be appended to the
existing children of the
+ * {@link #getParent() parent}, and will be given the appropriate
same-name-sibling index.
+ * <p>
+ * This is equivalent to calling
<code>node.copyTo(node.getParent())</code>
+ * </p>
+ *
+ * @throws IllegalArgumentException if this is the root node
+ */
+ public void cloneNode() {
+ copyTo(getParent());
+ }
+
+ /**
+ * Remove this node from it's parent. Note that locations are
<i>not</i> updated, since they will be updated if this node
+ * is added to a different parent. However, the locations of same-name-siblings
under the parent <i>are</i> updated.
+ */
+ protected void remove() {
+ assert !isStale();
+ assert this.parent != null;
+ assert this.parent.isLoaded();
+ assert this.parent.childrenByName != null;
+ assert this.parent.childrenByName != cache.NO_CHILDREN;
+ this.parent.markAsChanged();
+ Name name = getName();
+ List<Node<Payload, PropertyPayload>> childrenWithSameName =
this.parent.childrenByName.get(name);
+ this.parent = null;
+ if (childrenWithSameName.size() == 1) {
+ // No same-name-siblings ...
+ childrenWithSameName.clear();
+ } else {
+ // There is at least one other sibling with the same name ...
+ int lastIndex = childrenWithSameName.size() - 1;
+ assert lastIndex > 0;
+ int index = childrenWithSameName.indexOf(this);
+ // remove this node ...
+ childrenWithSameName.remove(index);
+ if (index != lastIndex) {
+ // There are same-name-siblings that have higher SNS indexes that
this node had ...
+ for (int i = index; i != lastIndex; ++i) {
+ Node<Payload, PropertyPayload> sibling =
childrenWithSameName.get(i);
+ Path.Segment segment = cache.pathFactory.createSegment(name, i +
1);
+ sibling.updateLocation(segment);
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove this node from it's parent and destroy it's contents. The
location of sibling nodes with the same name will be
+ * updated, and the node and all nodes below it will be destroyed and removed
from the cache.
+ */
+ public void destroy() {
+ assert !isStale();
+ // Check authorization ...
+ cache.authorizer.checkPermissions(getPath(), Action.REMOVE);
+ // Remove the node from its parent ...
+ remove();
+ // This node was successfully removed, so now remove it from the cache ...
+ cache.recordDelete(this);
+ }
+
+ public final boolean isRoot() {
+ return this.parent == null;
+ }
+
+ /**
+ * Determine whether this node is stale because it was dropped from the cache.
+ *
+ * @return true if the node is stale and should no longer be used
+ */
+ public boolean isStale() {
+ // Find the root of this node ...
+ Node<?, ?> node = this;
+ while (node.parent != null) {
+ node = node.parent;
+ }
+ // The root of this branch MUST be the actual root of the cache
+ return node != cache.root;
+ }
+
+ /**
+ * Get this node's parent node.
+ *
+ * @return the parent node
+ */
+ public final Node<Payload, PropertyPayload> getParent() {
+ assert !isStale();
+ return parent;
+ }
+
+ /**
+ * @return nodeId
+ */
+ public final NodeId getNodeId() {
+ return nodeId;
+ }
+
+ /**
+ * Get the name of this node, without any same-name-sibling index.
+ *
+ * @return the name; never null
+ */
+ public final Name getName() {
+ return location.getPath().getLastSegment().getName();
+ }
+
+ /**
+ * Get the {@link Path.Segment path segment} for this node.
+ *
+ * @return the path segment; never null
+ */
+ public final Path.Segment getSegment() {
+ return location.getPath().getLastSegment();
+ }
+
+ /**
+ * Get the current path to this node.
+ *
+ * @return the current path; never null
+ */
+ public final Path getPath() {
+ return location.getPath();
+ }
+
+ /**
+ * Get the current location for this node.
+ *
+ * @return the current location; never null
+ */
+ public final Location getLocation() {
+ return location;
+ }
+
+ /**
+ * Get the child with the supplied segment.
+ *
+ * @param segment the segment of the child
+ * @return the child with the supplied name and SNS index, or null if the
children have not yet been loaded
+ * @throws PathNotFoundException if the children have been loaded and the child
does not exist
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public Node<Payload, PropertyPayload> getChild( Path.Segment segment ) {
+ return getChild(segment.getName(), segment.getIndex());
+ }
+
+ /**
+ * Get the first child matching the name and lowest SNS index
+ *
+ * @param name the name of the child
+ * @return the first child with the supplied name, or null if the children have
not yet been loaded
+ * @throws PathNotFoundException if the children have been loaded and the child
does not exist
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public Node<Payload, PropertyPayload> getFirstChild( Name name ) {
+ return getChild(name, 1);
+ }
+
+ /**
+ * Get the child with the supplied name and SNS index.
+ *
+ * @param name the name of the child
+ * @param sns the same-name-sibling index; must be 1 or more
+ * @return the child with the supplied name and SNS index; never null
+ * @throws PathNotFoundException if the children have been loaded and the child
does not exist
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public Node<Payload, PropertyPayload> getChild( Name name,
+ int sns ) {
+ load();
+ List<Node<Payload, PropertyPayload>> children =
childrenByName.get(name); // never null
+ try {
+ return children.get(sns - 1); // SNS is 1-based, index is 0-based
+ } catch (IndexOutOfBoundsException e) {
+ Path missingPath = cache.pathFactory.create(getPath(), name, sns);
+ throw new PathNotFoundException(Location.create(missingPath),
getPath());
+ }
+ }
+
+ /**
+ * Get an iterator over the children that have the supplied name.
+ *
+ * @param name the of the child nodes to be returned; may not be null
+ * @return an unmodifiable iterator over the cached children that have the
supplied name; never null but possibly empty
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public Iterable<Node<Payload, PropertyPayload>> getChildren( Name
name ) {
+ load();
+ final Collection<Node<Payload, PropertyPayload>> children =
childrenByName.get(name);
+ return new Iterable<Node<Payload, PropertyPayload>>() {
+ public Iterator<Node<Payload, PropertyPayload>> iterator() {
+ return new ReadOnlyIterator<Node<Payload,
PropertyPayload>>(children.iterator());
+ }
+ };
+ }
+
+ /**
+ * Get an iterator over the children.
+ *
+ * @return an unmodifiable iterator over the cached children; never null
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public Iterable<Node<Payload, PropertyPayload>> getChildren() {
+ load();
+ final Collection<Node<Payload, PropertyPayload>> children =
childrenByName.values();
+ return new Iterable<Node<Payload, PropertyPayload>>() {
+ public Iterator<Node<Payload, PropertyPayload>> iterator() {
+ return new ReadOnlyIterator<Node<Payload,
PropertyPayload>>(children.iterator());
+ }
+ };
+ }
+
+ /**
+ * Get the number of children.
+ *
+ * @return the number of children in the cache
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public int getChildrenCount() {
+ load();
+ return childrenByName.size();
+ }
+
+ /**
+ * Get the number of children that have the same supplied name.
+ *
+ * @param name the name of the children to count
+ * @return the number of children in the cache
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public int getChildrenCount( Name name ) {
+ load();
+ return childrenByName.get(name).size();
+ }
+
+ /**
+ * Determine if this node is a leaf node with no children.
+ *
+ * @return true if this node has no children
+ * @throws RepositorySourceException if there is a problem loading this
node's information from the store
+ */
+ public boolean isLeaf() {
+ load();
+ return childrenByName.isEmpty();
+ }
+
+ /**
+ * Get from this node the property with the supplied name.
+ *
+ * @param name the property name; may not be null
+ * @return the property with the supplied name, or null if there is no such
property on this node
+ */
+ public PropertyInfo<PropertyPayload> getProperty( Name name ) {
+ load();
+ return properties.get(name);
+ }
+
+ /**
+ * Set the supplied property information on this node.
+ *
+ * @param property the new property; may not be null
+ * @param isMultiValued true if the property is multi-valued
+ * @param payload the optional payload for this property; may be null
+ * @return the previous information for the property, or null if there was no
previous property
+ */
+ public PropertyInfo<PropertyPayload> setProperty( Property property,
+ boolean isMultiValued,
+ PropertyPayload payload ) {
+ load();
+ cache.authorizer.checkPermissions(getPath(), Action.SET_PROPERTY);
+ Name name = property.getName();
+ PropertyInfo<PropertyPayload> previous = properties.get(name);
+ PropertyId id = null;
+ PropertyStatus status = null;
+ if (previous != null) {
+ id = previous.getPropertyId();
+ status = previous.getStatus(); // keep NEW or CHANGED status, but
UNCHANGED -> CHANGED
+ if (status == PropertyStatus.UNCHANGED) status = PropertyStatus.CHANGED;
+ } else {
+ id = new PropertyId(getNodeId(), name);
+ status = PropertyStatus.NEW;
+ }
+ PropertyInfo<PropertyPayload> info = new
PropertyInfo<PropertyPayload>(id, property, isMultiValued, status, payload);
+ properties.put(name, info);
+ cache.operations.set(property).on(location);
+ markAsChanged();
+ return previous;
+ }
+
+ /**
+ * Remove a property from this node.
+ *
+ * @param name the name of the property to be removed; may not be null
+ * @return the previous information for the property, or null if there was no
previous property
+ */
+ public PropertyInfo<PropertyPayload> removeProperty( Name name ) {
+ load();
+ PropertyInfo<PropertyPayload> results = properties.remove(name);
+ markAsChanged();
+ cache.operations.remove(name).on(location);
+ return results;
+ }
+
+ /**
+ * Get the names of the properties on this node.
+ *
+ * @return the names of the properties; never null
+ */
+ public Set<Name> getPropertyNames() {
+ load();
+ return properties.keySet();
+ }
+
+ /**
+ * Get the information for each of the properties on this node.
+ *
+ * @return the information for each of the properties; never null
+ */
+ public Collection<PropertyInfo<PropertyPayload>> getProperties() {
+ load();
+ return properties.values();
+ }
+
+ /**
+ * Get the number of properties owned by this node.
+ *
+ * @return the number of properties; never negative
+ */
+ public int getPropertyCount() {
+ load();
+ return properties.size();
+ }
+
+ public boolean isAtOrBelow( Node<Payload, PropertyPayload> other ) {
+ Node<Payload, PropertyPayload> node = this;
+ while (node != null) {
+ if (node == other) return true;
+ node = node.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * @return payload
+ */
+ public Payload getPayload() {
+ return payload;
+ }
+
+ /**
+ * @param payload Sets payload to the specified value.
+ */
+ public void setPayload( Payload payload ) {
+ this.payload = payload;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public final int hashCode() {
+ return nodeId.hashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @SuppressWarnings( "unchecked" )
+ @Override
+ public boolean equals( Object obj ) {
+ if (obj == this) return true;
+ if (obj instanceof Node) {
+ Node<Payload, PropertyPayload> that = (Node<Payload,
PropertyPayload>)obj;
+ if (!this.nodeId.equals(that.nodeId)) return false;
+ return this.location.equals(that.location);
+ }
+ return false;
+ }
+
+ /**
+ * Utility method to obtain a string representation that uses the namespace
prefixes where appropriate.
+ *
+ * @param registry the namespace registry, or null if no prefixes should be used
+ * @return the string representation; never null
+ */
+ public String getString( NamespaceRegistry registry ) {
+ return "Cached node <" + nodeId + "> at " +
location.getString(registry);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return getString(null);
+ }
+
+ /**
+ * Visit all nodes in the cache that are already loaded
+ *
+ * @param visitor the visitor; may not be null
+ */
+ public void onLoadedNodes( NodeVisitor<Payload, PropertyPayload> visitor )
{
+ if (this.isLoaded()) {
+ // Create a queue. This queue will contain all the nodes to be visited,
+ // so if loading is not forced, then the queue should only contain
already-loaded nodes...
+ LinkedList<Node<Payload, PropertyPayload>> queue = new
LinkedList<Node<Payload, PropertyPayload>>();
+ queue.add(this);
+ while (!queue.isEmpty()) {
+ Node<Payload, PropertyPayload> node = queue.poll();
+ // Get an iterator over the children *before* we visit the node
+ Iterator<Node<Payload, PropertyPayload>> iter =
node.getChildren().iterator();
+ // Visit this node ...
+ if (visitor.visit(node)) {
+ // Visit the children ...
+ int index = -1;
+ while (iter.hasNext()) {
+ Node<Payload, PropertyPayload> child = iter.next();
+ if (child.isLoaded()) {
+ queue.add(++index, child);
+ }
+ }
+ }
+ }
+ }
+ visitor.finish();
+ }
+
+ /**
+ * Visit all loaded and unloaded nodes in the cache.
+ *
+ * @param visitor the visitor; may not be null
+ */
+ public void onCachedNodes( NodeVisitor<Payload, PropertyPayload> visitor )
{
+ // Create a queue. This queue will contain all the nodes to be visited,
+ // so if loading is not forced, then the queue should only contain
already-loaded nodes...
+ LinkedList<Node<Payload, PropertyPayload>> queue = new
LinkedList<Node<Payload, PropertyPayload>>();
+ queue.add(this);
+ while (!queue.isEmpty()) {
+ Node<Payload, PropertyPayload> node = queue.poll();
+ // Get an iterator over the children *before* we visit the node
+ Iterator<Node<Payload, PropertyPayload>> iter =
node.getChildren().iterator();
+ // Visit this node ...
+ if (visitor.visit(node)) {
+ // Visit the children ...
+ int index = -1;
+ while (iter.hasNext()) {
+ Node<Payload, PropertyPayload> child = iter.next();
+ queue.add(++index, child);
+ }
+ }
+ }
+ visitor.finish();
+ }
+
+ /**
+ * Visit all changed nodes in the cache.
+ *
+ * @param visitor the visitor; may not be null
+ */
+ public void onChangedNodes( NodeVisitor<Payload, PropertyPayload> visitor )
{
+ if (this.isChanged(true)) {
+ // Create a queue. This queue will contain all the nodes to be visited
...
+ LinkedList<Node<Payload, PropertyPayload>> changedNodes = new
LinkedList<Node<Payload, PropertyPayload>>();
+ changedNodes.add(this);
+ while (!changedNodes.isEmpty()) {
+ Node<Payload, PropertyPayload> node = changedNodes.poll();
+ // Visit this node ...
+ boolean visitChildren = true;
+ if (node.changed) {
+ visitChildren = visitor.visit(node);
+ }
+ if (visitChildren && node.changedBelow) {
+ // Visit the children ...
+ int index = -1;
+ Iterator<Node<Payload, PropertyPayload>> iter =
node.getChildren().iterator();
+ while (iter.hasNext()) {
+ Node<Payload, PropertyPayload> child = iter.next();
+ if (node.isChanged(true)) {
+ changedNodes.add(++index, child);
+ }
+ }
+ }
+ }
+ }
+ visitor.finish();
+ }
+
+ /**
+ * Obtain a snapshot of the structure below this node.
+ *
+ * @param pathsOnly true if the snapshot should only include paths, or false if
the entire locations should be included
+ * @return the snapshot
+ */
+ public StructureSnapshot<PropertyPayload> getSnapshot( final boolean
pathsOnly ) {
+ final List<Snapshot<PropertyPayload>> snapshots = new
ArrayList<Snapshot<PropertyPayload>>();
+ onCachedNodes(new NodeVisitor<Payload, PropertyPayload>() {
+ @Override
+ public boolean visit( Node<Payload, PropertyPayload> node ) {
+ snapshots.add(new Snapshot<PropertyPayload>(node, pathsOnly,
true));
+ return node.isLoaded();
+ }
+ });
+ return new
StructureSnapshot<PropertyPayload>(cache.context.getNamespaceRegistry(),
+
Collections.unmodifiableList(snapshots));
+ }
+ }
+
+ public static enum PropertyStatus {
+ NEW,
+ CHANGED,
+ UNCHANGED;
+ }
+
+ @Immutable
+ public static final class PropertyInfo<PropertyPayload> {
+ private final PropertyId propertyId;
+ private final Property property;
+ private final PropertyStatus status;
+ private final boolean multiValued;
+ private final PropertyPayload payload;
+
+ protected PropertyInfo( PropertyId propertyId,
+ Property property,
+ boolean multiValued,
+ PropertyStatus status,
+ PropertyPayload payload ) {
+ assert propertyId != null;
+ assert property != null;
+ assert status != null;
+ this.propertyId = propertyId;
+ this.property = property;
+ this.status = status;
+ this.multiValued = multiValued;
+ this.payload = payload;
+ }
+
+ /**
+ * Get the status of this property.
+ *
+ * @return the current status; never null
+ */
+ public PropertyStatus getStatus() {
+ return status;
+ }
+
+ /**
+ * Get the name of the property.
+ *
+ * @return the propert name; never null
+ */
+ public Name getName() {
+ return propertyId.getPropertyName();
+ }
+
+ /**
+ * Get the Graph API property object containing the values.
+ *
+ * @return the property object; never null
+ */
+ public Property getProperty() {
+ return property;
+ }
+
+ /**
+ * Get the identifier for this property.
+ *
+ * @return the property identifier; never null
+ */
+ public PropertyId getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Get the payload for this property.
+ *
+ * @return the payload; may be null if there is no payload
+ */
+ public PropertyPayload getPayload() {
+ return payload;
+ }
+
+ /**
+ * Determine whether this property has multiple values
+ *
+ * @return multiValued
+ */
+ public boolean isMultiValued() {
+ return multiValued;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return propertyId.hashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals( Object obj ) {
+ if (obj == this) return true;
+ if (obj instanceof PropertyInfo) {
+ PropertyInfo<?> that = (PropertyInfo<?>)obj;
+ return propertyId.equals(that.getPropertyId());
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(propertyId);
+ if (payload != null) sb.append(payload);
+ if (property.isSingle()) {
+ sb.append(" with value ");
+ } else {
+ sb.append(" with values ");
+ }
+ sb.append(property.getValuesAsArray());
+ return sb.toString();
+ }
+ }
+
+ /**
+ * The node visitor.
+ *
+ * @param <NodePayload> the type of node payload object
+ * @param <PropertyPayloadType> the type of property payload object
+ */
+ @NotThreadSafe
+ public static abstract class NodeVisitor<NodePayload, PropertyPayloadType> {
+ /**
+ * Visit the supplied node, returning whether the children should be visited.
+ *
+ * @param node the node to be visited; never null
+ * @return true if the node's children should be visited, or false if no
children should be visited
+ */
+ public abstract boolean visit( Node<NodePayload, PropertyPayloadType> node
);
+
+ /**
+ * Method that should be called after all visiting has been done successfully
(with no exceptions), including when no
+ * nodes were visited.
+ */
+ public void finish() {
+ }
+ }
+
+ /**
+ * An abstract base class for visitors that need to load nodes using a single batch
for all read operations. To use, simply
+ * subclass and supply a {@link #visit(Node)} implementation that calls {@link
#load(Node)} for each node that is to be
+ * loaded. When the visitor is {@link #finish() finished}, all of these nodes will be
read from the store and loaded. The
+ * {@link #finishNodeAfterLoading(Node)} is called after each node is loaded,
allowing the subclass to perform an operation on
+ * the newly-loaded nodes.
+ */
+ @NotThreadSafe
+ protected abstract class LoadNodesVisitor extends NodeVisitor<Payload,
PropertyPayload> {
+ private Graph.Batch batch = GraphSession.this.store.batch();
+ private List<Node<Payload, PropertyPayload>> nodesToLoad = new
LinkedList<Node<Payload, PropertyPayload>>();
+
+ /**
+ * Method that signals that the supplied node should be loaded (if it is not
already loaded). This method should be called
+ * from within the {@link #visit(Node)} method of the subclass.
+ *
+ * @param node the node that should be loaded (if it is not already)
+ */
+ protected void load( Node<Payload, PropertyPayload> node ) {
+ if (node != null && !node.isLoaded()) {
+ nodesToLoad.add(node);
+ batch.read(node.getLocation());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.NodeVisitor#finish()
+ */
+ @Override
+ public void finish() {
+ super.finish();
+ if (!nodesToLoad.isEmpty()) {
+ // Read all of the children in one batch ...
+ Results results = batch.execute();
+ // Now load all of the children into the correct node ...
+ for (Node<Payload, PropertyPayload> childToBeRead : nodesToLoad) {
+ org.jboss.dna.graph.Node persistentNode =
results.getNode(childToBeRead.getLocation());
+ nodeOperations.update(persistentNode, childToBeRead);
+ finishNodeAfterLoading(childToBeRead);
+ }
+ }
+ }
+
+ /**
+ * Method that is called on each node loaded by this visitor. This method does
nothing by default.
+ *
+ * @param node the just-loaded node; never null
+ */
+ protected void finishNodeAfterLoading( Node<Payload, PropertyPayload> node
) {
+ // do nothing
+ }
+ }
+
+ /**
+ * A visitor that ensures that all children of a node are loaded, and provides a hook
to {@link #finishNodeAfterLoading(Node)
+ * post-process the parent}.
+ */
+ @NotThreadSafe
+ protected class LoadAllChildrenVisitor extends LoadNodesVisitor {
+ private List<Node<Payload, PropertyPayload>> parentsVisited = new
LinkedList<Node<Payload, PropertyPayload>>();
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.NodeVisitor#visit(GraphSession.Node)
+ */
+ @Override
+ public boolean visit( Node<Payload, PropertyPayload> node ) {
+ parentsVisited.add(node);
+ Iterator<Node<Payload, PropertyPayload>> iter =
node.getChildren().iterator();
+ while (iter.hasNext()) {
+ load(iter.next());
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see GraphSession.LoadNodesVisitor#finish()
+ */
+ @Override
+ public void finish() {
+ super.finish();
+ for (Node<Payload, PropertyPayload> parent : parentsVisited) {
+ finishParentAfterLoading(parent);
+ }
+ }
+
+ /**
+ * Method that is called at the end of the {@link #finish()} stage with each
parent node whose children were all loaded.
+ *
+ * @param parentNode the parent of the just-loaded children; never null
+ */
+ protected void finishParentAfterLoading( Node<Payload, PropertyPayload>
parentNode ) {
+ // do nothing
+ }
+ }
+
+ protected static final class Snapshot<PropertyPayload> {
+ private final Location location;
+ private final boolean isLoaded;
+ private final boolean isChanged;
+ private final Collection<PropertyInfo<PropertyPayload>> properties;
+ private final NodeId id;
+
+ protected Snapshot( Node<?, PropertyPayload> node,
+ boolean pathsOnly,
+ boolean includeProperties ) {
+ this.location = pathsOnly && node.getLocation().hasIdProperties() ?
Location.create(node.getLocation().getPath()) : node.getLocation();
+ this.isLoaded = node.isLoaded();
+ this.isChanged = node.isChanged(false);
+ this.id = node.getNodeId();
+ this.properties = includeProperties ? node.getProperties() : null;
+ }
+
+ /**
+ * @return location
+ */
+ public Location getLocation() {
+ return location;
+ }
+
+ /**
+ * @return isChanged
+ */
+ public boolean isChanged() {
+ return isChanged;
+ }
+
+ /**
+ * @return isLoaded
+ */
+ public boolean isLoaded() {
+ return isLoaded;
+ }
+
+ /**
+ * @return id
+ */
+ public NodeId getId() {
+ return id;
+ }
+
+ /**
+ * @return properties
+ */
+ public Collection<PropertyInfo<PropertyPayload>> getProperties() {
+ return properties;
+ }
+ }
+
+ /**
+ * A read-only visitor that walks the cache to obtain a snapshot of the cache
structure. The resulting snapshot contains the
+ * location of each node in the tree, including unloaded nodes.
+ *
+ * @param <PropertyPayload> the property payload
+ */
+ @Immutable
+ public static final class StructureSnapshot<PropertyPayload> implements
Iterable<Snapshot<PropertyPayload>> {
+ private final List<Snapshot<PropertyPayload>> snapshotsInPreOrder;
+ private final NamespaceRegistry registry;
+
+ protected StructureSnapshot( NamespaceRegistry registry,
+ List<Snapshot<PropertyPayload>>
snapshotsInPreOrder ) {
+ assert snapshotsInPreOrder != null;
+ this.snapshotsInPreOrder = snapshotsInPreOrder;
+ this.registry = registry;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Iterable#iterator()
+ */
+ public Iterator<Snapshot<PropertyPayload>> iterator() {
+ return snapshotsInPreOrder.iterator();
+ }
+
+ /**
+ * Get the Location for every node in this cache
+ *
+ * @return the node locations (in pre-order)
+ */
+ public List<Snapshot<PropertyPayload>> getSnapshotsInPreOrder() {
+ return snapshotsInPreOrder;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ int maxLength = 0;
+ for (Snapshot<PropertyPayload> snapshot : this) {
+ String path = snapshot.getLocation().getPath().getString(registry);
+ maxLength = Math.max(maxLength, path.length());
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Snapshot<PropertyPayload> snapshot : this) {
+ Location location = snapshot.getLocation();
+ sb.append(StringUtil.justifyLeft(location.getPath().getString(registry),
maxLength, ' '));
+ // Append the node identifier ...
+ sb.append(StringUtil.justifyRight(snapshot.getId().toString(), 10, '
'));
+ // Append the various state flags
+ if (snapshot.isChanged()) sb.append(" (*)");
+ else if (!snapshot.isLoaded()) sb.append(" (-)");
+ else sb.append(" ");
+ // Append the location's identifier properties ...
+ if (location.hasIdProperties()) {
+ sb.append(" ");
+ if (location.getIdProperties().size() == 1 &&
location.getUuid() != null) {
+ sb.append(location.getUuid());
+ } else {
+ boolean first = true;
+ sb.append('[');
+ for (Property property : location) {
+ sb.append(property.getString(registry));
+ if (first) first = false;
+ else sb.append(", ");
+ }
+ sb.append(']');
+ }
+ }
+ // Append the property information ...
+ if (snapshot.getProperties() != null) {
+ boolean first = true;
+ sb.append(" {");
+ for (PropertyInfo<?> info : snapshot.getProperties()) {
+ if (first) first = false;
+ else sb.append("} {");
+ sb.append(info.getProperty().getString(registry));
+ }
+ sb.append("}");
+ }
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ @NotThreadSafe
+ protected static final class RefreshState<Payload, PropertyPayload> {
+ private final Set<Node<Payload, PropertyPayload>> refresh = new
HashSet<Node<Payload, PropertyPayload>>();
+
+ public void markAsRequiringRefresh( Node<Payload, PropertyPayload> node )
{
+ refresh.add(node);
+ }
+
+ public boolean requiresRefresh( Node<Payload, PropertyPayload> node ) {
+ return refresh.contains(node);
+ }
+
+ public Set<Node<Payload, PropertyPayload>> getNodesToBeRefreshed() {
+ return refresh;
+ }
+ }
+
+ @NotThreadSafe
+ protected final static class Dependencies {
+ private Set<NodeId> requireChangesTo;
+ private NodeId movedFrom;
+
+ public Dependencies() {
+ }
+
+ /**
+ * @return movedFrom
+ */
+ public NodeId getMovedFrom() {
+ return movedFrom;
+ }
+
+ /**
+ * Record that this node is being moved from one parent to another. This method
only records the original parent, so
+ * subsequent calls to this method do nothing.
+ *
+ * @param movedFrom the identifier of the original parent of this node
+ */
+ public void setMovedFrom( NodeId movedFrom ) {
+ if (this.movedFrom == null) this.movedFrom = movedFrom;
+ }
+
+ /**
+ * @return requireChangesTo
+ */
+ public Set<NodeId> getRequireChangesTo() {
+ return requireChangesTo != null ? requireChangesTo :
Collections.<NodeId>emptySet();
+ }
+
+ /**
+ * @param other the other node that changes are dependent upon
+ */
+ public void addRequireChangesTo( NodeId other ) {
+ if (other == null) return;
+ if (requireChangesTo == null) {
+ requireChangesTo = new HashSet<NodeId>();
+ }
+ requireChangesTo.add(other);
+ }
+ }
+
+ /**
+ * An immutable identifier for a node, used within the {@link GraphSession}.
+ */
+ @Immutable
+ public final static class NodeId {
+
+ private final long nodeId;
+
+ /**
+ * Create a new node identifier.
+ *
+ * @param nodeId unique identifier
+ */
+ public NodeId( long nodeId ) {
+ this.nodeId = nodeId;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return (int)nodeId;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals( Object obj ) {
+ if (obj == this) return true;
+ if (obj instanceof NodeId) {
+ NodeId that = (NodeId)obj;
+ return this.nodeId == that.nodeId;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return Long.toString(nodeId);
+ }
+ }
+
+ /**
+ * An immutable identifier for a property on a node, used within the {@link
GraphSession}.
+ */
+ @Immutable
+ public final static class PropertyId {
+ private final NodeId nodeId;
+ private final Name propertyName;
+ private final int hc;
+
+ public PropertyId( NodeId nodeId,
+ Name propertyName ) {
+ this.nodeId = nodeId;
+ this.propertyName = propertyName;
+ this.hc = HashCode.compute(this.nodeId, this.propertyName);
+ }
+
+ /**
+ * Get the identifier of the node on which the property exists.
+ *
+ * @return the node identifier; never null
+ */
+ public NodeId getNodeId() {
+ return nodeId;
+ }
+
+ /**
+ * Get the name of the property.
+ *
+ * @return the property name; never null
+ */
+ public Name getPropertyName() {
+ return propertyName;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return hc;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals( Object obj ) {
+ if (obj == this) return true;
+ if (obj instanceof PropertyId) {
+ PropertyId that = (PropertyId)obj;
+ if (this.hc != that.hc) return false;
+ if (!this.nodeId.equals(that.nodeId)) return false;
+ return this.propertyName.equals(that.propertyName);
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return this.nodeId.toString() + '@' + this.propertyName.toString();
+ }
+ }
+}
Property changes on:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/GraphSession.java
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ LF
Added:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/InvalidStateException.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/InvalidStateException.java
(rev 0)
+++
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/InvalidStateException.java 2009-07-09
16:23:09 UTC (rev 1081)
@@ -0,0 +1,65 @@
+/*
+ * JBoss DNA (
http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site:
http://www.fsf.org.
+ */
+package org.jboss.dna.graph.session;
+
+/**
+ *
+ */
+public class InvalidStateException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ */
+ public InvalidStateException() {
+ }
+
+ /**
+ * @param message
+ */
+ public InvalidStateException( String message ) {
+ super(message);
+
+ }
+
+ /**
+ * @param cause
+ */
+ public InvalidStateException( Throwable cause ) {
+ super(cause);
+
+ }
+
+ /**
+ * @param message
+ * @param cause
+ */
+ public InvalidStateException( String message,
+ Throwable cause ) {
+ super(message, cause);
+
+ }
+
+}
Property changes on:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/InvalidStateException.java
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ LF
Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/ValidationException.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/ValidationException.java
(rev 0)
+++
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/ValidationException.java 2009-07-09
16:23:09 UTC (rev 1081)
@@ -0,0 +1,65 @@
+/*
+ * JBoss DNA (
http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site:
http://www.fsf.org.
+ */
+package org.jboss.dna.graph.session;
+
+/**
+ *
+ */
+public class ValidationException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ */
+ public ValidationException() {
+ }
+
+ /**
+ * @param message
+ */
+ public ValidationException( String message ) {
+ super(message);
+
+ }
+
+ /**
+ * @param cause
+ */
+ public ValidationException( Throwable cause ) {
+ super(cause);
+
+ }
+
+ /**
+ * @param message
+ * @param cause
+ */
+ public ValidationException( String message,
+ Throwable cause ) {
+ super(message, cause);
+
+ }
+
+}
Property changes on:
trunk/dna-graph/src/main/java/org/jboss/dna/graph/session/ValidationException.java
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ LF
Modified: trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties
===================================================================
--- trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties 2009-07-09
16:22:06 UTC (rev 1080)
+++ trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties 2009-07-09
16:23:09 UTC (rev 1081)
@@ -49,7 +49,7 @@
pathExpressionHasInvalidMatch = Invalid match expression "{0}" in the path
expression "{1}"
messageDigestNotFound = The "{0}" message digest algorithm could not be found
unableToAccessResourceFileFromClassLoader = Unable to access "{0}" resource
from the class loader
-pathNotFoundExceptionLowestExistingLocationFound = {0}; lowest existing path is {1}
+pathNotFoundExceptionLowestExistingLocationFound = Unable to find "{0}"; lowest
existing path is "{1}"
executingRequest = Executing {0}
executedRequest = Executed {0}
@@ -101,9 +101,14 @@
federatedSourceDoesNotSupportCloningWorkspaces = {0} is a source that does not allow
cloning workspaces
federatedSourceDoesNotSupportDestroyingWorkspaces = {0} is a source that does not allow
destroying workspaces
unableToProjectSourceInformationIntoWorkspace = Unable to project source information at
{0} in the "{1} federated repository source using projection {2}
-unableToCreateNodeUnderPlaceholder = Unable to create node "{0}" under {1} in
the "{2}" workspace of the "{3}" federated repository because the
parent is a placeholder
-unableToUpdatePlaceholder = Unable to update node {0} in the "{1}" workspace of
the "{2}" federated repository because the node is a placeholder
-unableToDeletePlaceholder = Unable to delete node {0} in the "{1}" workspace of
the "{2}" federated repository because the node is a placeholder
-copyLimitedToBeWithinSingleSource = Unable to copy {0} in the "{1}" workspace
of the "{3}" federated repository into {2} in the "{3}" workspace:
copy is only supported when the original and new locations are within the same source
-moveLimitedToBeWithinSingleSource = Unable to move {0} in the "{1}" workspace
of the "{3}" federated repository into {2} in the "{3}" workspace:
move is only supported when the original and new locations are within the same source
+unableToCreateNodeUnderPlaceholder = Unable to create node "{0}" under {1} in
the "{2}" workspace of the "{3}" federarted repository because the
parent is a placeholder
+unableToUpdatePlaceholder = Unable to update node {0} in the "{1}" workspace of
the "{2}" federarted repository because the node is a placeholder
+unableToDeletePlaceholder = Unable to delete node {0} in the "{1}" workspace of
the "{2}" federarted repository because the node is a placeholder
+copyLimitedToBeWithinSingleSource = Unable to copy {0} in the "{1}" workspace
of the "{3}" federarted repository into {2} in the "{3}" workspace:
copy is only supported when the original and new locations are within the same source
+moveLimitedToBeWithinSingleSource = Unable to move {0} in the "{1}" workspace
of the "{3}" federarted repository into {2} in the "{3}" workspace:
move is only supported when the original and new locations are within the same source
cloneLimitedToBeWithinSingleSource = Unable to clone {0} in the "{1}" workspace
of the "{3}" federated repository into {2} in the "{3}" workspace:
clone is only supported when the original and new locations are within the same source
+
+# Session
+unableToRefreshBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch = Unable to
refresh "{0}" in workspace "{1}" because it contains changes that
depend on changes to nodes outside of this branch
+unableToSaveBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch = Unable to save
"{0}" in workspace "{1}" because it contains changes that depend on
changes to nodes outside of this branch
+nodeHasAlreadyBeenRemovedFromThisSession = Node "{0}" in workspace "{1}
has already been removed from this session
Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/GraphCacheTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/GraphCacheTest.java
(rev 0)
+++
trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/GraphCacheTest.java 2009-07-09
16:23:09 UTC (rev 1081)
@@ -0,0 +1,574 @@
+/*
+ * JBoss DNA (
http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * JBoss DNA is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site:
http://www.fsf.org.
+ */
+package org.jboss.dna.graph.session;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.RepositorySourceException;
+import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.session.GraphSession.Node;
+import org.jboss.dna.graph.session.GraphSession.Operations;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class GraphCacheTest {
+
+ protected ExecutionContext context;
+ protected InMemoryRepositorySource source;
+ private Graph store;
+ private GraphSession<Object, Object> cache;
+ private PathFactory pathFactory;
+ protected int numberOfConnections;
+
+ @Before
+ public void beforeEach() throws Exception {
+ context = new ExecutionContext();
+ pathFactory = context.getValueFactories().getPathFactory();
+ source = new InMemoryRepositorySource();
+ source.setName("store");
+ // Use a connection factory so we can count the number of connections that were
made
+ RepositoryConnectionFactory connectionFactory = new RepositoryConnectionFactory()
{
+ public RepositoryConnection createConnection( String sourceName ) throws
RepositorySourceException {
+ if (source.getName().equals(sourceName)) {
+ ++numberOfConnections;
+ return source.getConnection();
+ }
+ return null;
+ }
+ };
+ store = Graph.create(source.getName(), connectionFactory, context);
+
+ // Load the store with content ...
+
store.importXmlFrom(getClass().getClassLoader().getResourceAsStream("cars.xml")).into("/");
+ numberOfConnections = 0; // reset the number of connections
+
+ Operations<Object, Object> nodeOps = null; // use default
+ String workspaceName = null; // use current
+ cache = new GraphSession<Object, Object>(store, workspaceName, nodeOps);
+ }
+
+ @Test
+ public void shouldHaveRootNodeWithCorrectNodeIdAndLocation() {
+ Node<Object, Object> node = cache.getRoot();
+ assertThat(node, is(notNullValue()));
+ assertThat(node.getNodeId(), is(notNullValue()));
+ assertThat(node.getLocation(), is(notNullValue()));
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldNotHaveAutomaticallyLoadedRootNode() {
+ assertThat(cache.getRoot().isLoaded(), is(false));
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldHaveRootNodeWithChildren() {
+ Node<Object, Object> node = cache.getRoot();
+ assertChildren(node, "Cars");
+ assertConnectionsUsed(1);
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldHaveNoExpirationIfSourceDoesNotHaveCachePolicy() {
+ Node<Object, Object> node = cache.getRoot();
+ node.load();
+ assertThat(node.getExpirationTimeInMillis(), is(Long.MAX_VALUE));
+ assertConnectionsUsed(1);
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldAutomaticallyLoadNodesWhenNavigatingToChildren() {
+ Node<Object, Object> root = cache.getRoot();
+ assertThat(root.isLoaded(), is(false));
+ assertChildren(root, "Cars"); // causes loading of the root node
+ assertThat(root.isLoaded(), is(true));
+ assertConnectionsUsed(1);
+
+ Node<Object, Object> cars = root.getChildren().iterator().next(); // only
one child
+ assertThat(cars.isLoaded(), is(false));
+ assertChildren(cars, "Hybrid", "Sports", "Luxury",
"Utility");
+ assertConnectionsUsed(1);
+ assertThat(cars.isLoaded(), is(true));
+ assertThat(cars.getParent(), is(sameInstance(root)));
+
+ Node<Object, Object> sports = cars.getChild(segment("Sports"));
+ assertThat(sports.isLoaded(), is(false));
+ assertChildren(sports, "Aston Martin DB9", "Infiniti G37");
+ assertConnectionsUsed(1);
+ assertThat(sports.isLoaded(), is(true));
+ assertThat(sports.getParent(), is(sameInstance(cars)));
+
+ Node<Object, Object> g37 = sports.getChild(segment("Infiniti
G37"));
+ assertThat(g37.isLoaded(), is(false));
+ assertChildren(g37);
+ assertConnectionsUsed(1);
+ assertThat(g37.isLoaded(), is(true));
+ assertThat(g37.isLeaf(), is(true));
+ assertThat(g37.getParent(), is(sameInstance(sports)));
+
+ // Try another branch ...
+ Node<Object, Object> utility =
cars.getChild(segment("Utility"));
+ assertThat(utility.isLoaded(), is(false));
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150");
+ assertConnectionsUsed(1);
+ assertThat(utility.isLoaded(), is(true));
+ assertThat(utility.getParent(), is(sameInstance(cars)));
+
+ Node<Object, Object> lr3 = utility.getChild(segment("Land Rover
LR3"));
+ assertThat(lr3.isLoaded(), is(false));
+ assertChildren(lr3);
+ assertConnectionsUsed(1);
+ assertThat(lr3.isLoaded(), is(true));
+ assertThat(lr3.isLeaf(), is(true));
+ assertThat(lr3.getParent(), is(sameInstance(utility)));
+
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldFindNodesByPath() {
+ Node<Object, Object> g37 =
cache.findNodeWith(path("/Cars/Sports/Infiniti G37")); // loads the node and all
parents
+ assertConnectionsUsed(1);
+ assertThat(g37.isLoaded(), is(true));
+ assertChildren(g37);
+ assertThat(g37.isLoaded(), is(true));
+ assertThat(g37.isLeaf(), is(true));
+
+ Node<Object, Object> sports = g37.getParent();
+ assertThat(sports.isLoaded(), is(true));
+ assertChildren(sports, "Aston Martin DB9", "Infiniti G37");
+
+ Node<Object, Object> cars = sports.getParent();
+ assertThat(cars.isLoaded(), is(true));
+ assertChildren(cars, "Hybrid", "Sports", "Luxury",
"Utility");
+
+ Node<Object, Object> root = cars.getParent();
+ assertThat(root, is(sameInstance(cache.getRoot())));
+ assertThat(root.isLoaded(), is(true));
+ assertChildren(root, "Cars"); // causes loading of the root node
+
+ assertNoMoreConnectionsUsed();
+
+ // Try another branch that should not be loaded...
+ Node<Object, Object> utility =
cars.getChild(segment("Utility"));
+ assertThat(utility.isLoaded(), is(false));
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150");
+ assertConnectionsUsed(1);
+ assertThat(utility.isLoaded(), is(true));
+ assertThat(utility.getParent(), is(sameInstance(cars)));
+
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldFindNodesById() {
+ cache.findNodeWith(path("/Cars/Sports/Infiniti G37")); // loads the
node and all parents
+ assertConnectionsUsed(1); // "Utility" was found because it is child of
"Cars" loaded when "Sports" was loaded
+
+ cache.getRoot().onCachedNodes(new GraphSession.NodeVisitor<Object,
Object>() {
+ @SuppressWarnings( "synthetic-access" )
+ @Override
+ public boolean visit( Node<Object, Object> node ) {
+ assertThat(cache.findNodeWith(node.getNodeId(), null),
is(sameInstance(node)));
+ return true;
+ }
+ });
+ }
+
+ @Test
+ public void shouldFindNodesByLocation() {
+ cache.findNodeWith(path("/Cars/Sports/Infiniti G37")); // loads the
node and all parents
+ assertConnectionsUsed(1); // "Utility" was found because it is child of
"Cars" loaded when "Sports" was loaded
+
+ cache.getRoot().onCachedNodes(new GraphSession.NodeVisitor<Object,
Object>() {
+ @SuppressWarnings( "synthetic-access" )
+ @Override
+ public boolean visit( Node<Object, Object> node ) {
+ assertThat(cache.findNodeWith(null, node.getLocation().getPath()),
is(sameInstance(node)));
+ return true;
+ }
+ });
+ }
+
+ @Test
+ public void shouldFindNodesByIdAfterClearing() {
+ Node<Object, Object> g37 =
cache.findNodeWith(path("/Cars/Sports/Infiniti G37")); // loads the node and all
parents
+ assertConnectionsUsed(1);
+ assertThat(g37.isLoaded(), is(true));
+ assertChildren(g37);
+ assertThat(g37.isLoaded(), is(true));
+ assertThat(g37.isLeaf(), is(true));
+
+ Node<Object, Object> sports = g37.getParent();
+ Node<Object, Object> cars = sports.getParent();
+ Node<Object, Object> root = cars.getParent();
+
+ assertNoMoreConnectionsUsed();
+
+ cache.getRoot().unload();
+
+ Node<Object, Object> g37_b = cache.findNodeWith(g37.getNodeId(),
g37.getPath());
+ assertConnectionsUsed(1);
+
+ Node<Object, Object> sports_b = g37_b.getParent();
+ Node<Object, Object> cars_b = sports_b.getParent();
+ Node<Object, Object> root_b = cars_b.getParent();
+ assertThat(g37_b, is(not(sameInstance(g37))));
+ assertThat(sports_b, is(not(sameInstance(sports))));
+ assertThat(cars_b, is(not(sameInstance(cars))));
+ assertThat(root_b, is(sameInstance(root)));
+ assertThat(g37_b.isLeaf(), is(true));
+ assertChildren(sports_b, "Aston Martin DB9", "Infiniti
G37");
+ assertChildren(cars_b, "Hybrid", "Sports",
"Luxury", "Utility");
+ assertChildren(root_b, "Cars");
+
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldMoveBranchAndRefresh() {
+ Node<Object, Object> sports =
cache.findNodeWith(path("/Cars/Sports"));
+ Node<Object, Object> utility =
cache.findNodeWith(path("/Cars/Utility"));
+ assertConnectionsUsed(1); // "Utility" was found because it is child of
"Cars" loaded when "Sports" was loaded
+
+ sports.moveTo(utility);
+ assertConnectionsUsed(1); // "Utility" was not fully loaded before
+ assertNoMoreConnectionsUsed();
+
+ Node<Object, Object> cars = cache.findNodeWith(path("/Cars"));
+
+ for (int i = 0; i != 3; ++i) {
+ assertChildren(cars, "Hybrid", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover
LR3", "Hummer H3", "Ford F-150", "Sports");
+ assertThat(sports.getParent(), is(utility));
+ assertThat(utility.getParent(), is(cars));
+
+ // Ensure that the changes were recorded appropriately ...
+ assertThat(cars.isChanged(false), is(true));
+ assertThat(sports.isChanged(false), is(false));
+ assertThat(utility.isChanged(false), is(true));
+ assertThat(cache.hasPendingChanges(), is(true));
+ assertThat(cache.changeDependencies.size(), is(1));
+ assertThat(cache.changeDependencies.get(sports.getNodeId()).getMovedFrom(),
is(cars.getNodeId()));
+ assertThat(cache.operations.isExecuteRequired(), is(true));
+
+ if (i == 0) {
+ // 1st time: Refreshing "Utility" shouldn't work because
"/Cars" was involved in the same move ...
+ try {
+ cache.refresh(utility, false);
+ fail("Expected exception from the call to refresh");
+ } catch (InvalidStateException e) {
+ // expected ...
+ }
+ } else if (i == 1) {
+ // 2nd time: refresh "/Cars" but keep the changes ...
+ cache.refresh(cars, true);
+ } else if (i == 2) {
+ // 3rd time:
+ cache.refresh(cars, false);
+ }
+ }
+
+ // Now the state should be back to the original representation, but we need to
refind the nodes ...
+ sports = cache.findNodeWith(path("/Cars/Sports"));
+ utility = cache.findNodeWith(path("/Cars/Utility"));
+ cars = cache.findNodeWith(path("/Cars"));
+
+ assertChildren(cars, "Hybrid", "Sports", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150");
+ assertThat(sports.getParent(), is(cars));
+ assertThat(utility.getParent(), is(cars));
+
+ // Now there should be no changes ...
+ assertNoChanges();
+ System.out.println(cache.root.getSnapshot(false));
+ }
+
+ @Test
+ public void shouldMoveBranchAndSaveBranch() {
+ Node<Object, Object> sports =
cache.findNodeWith(path("/Cars/Sports"));
+ Node<Object, Object> utility =
cache.findNodeWith(path("/Cars/Utility"));
+ assertConnectionsUsed(1); // "Utility" was found because it is child of
"Cars" loaded when "Sports" was loaded
+
+ sports.moveTo(utility);
+ assertConnectionsUsed(1); // "Utility" was not fully loaded before
+ assertNoMoreConnectionsUsed();
+
+ Node<Object, Object> cars = cache.findNodeWith(path("/Cars"));
+ Node<Object, Object> root = cache.getRoot();
+
+ for (int i = 0; i != 2; ++i) {
+ assertChildren(cars, "Hybrid", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover
LR3", "Hummer H3", "Ford F-150", "Sports");
+ assertThat(sports.getParent(), is(utility));
+ assertThat(utility.getParent(), is(cars));
+
+ // Ensure that the changes were recorded appropriately ...
+ assertThat(cars.isChanged(false), is(true));
+ assertThat(sports.isChanged(false), is(false));
+ assertThat(utility.isChanged(false), is(true));
+ assertThat(cache.hasPendingChanges(), is(true));
+ assertThat(cache.changeDependencies.size(), is(1));
+ assertThat(cache.changeDependencies.get(sports.getNodeId()).getMovedFrom(),
is(cars.getNodeId()));
+ assertThat(cache.operations.isExecuteRequired(), is(true));
+
+ if (i == 0) {
+ // 1st time: Saving "Utility" shouldn't work because
"/Cars" was involved in the same move ...
+ try {
+ cache.save(utility);
+ fail("Expected exception from the call to save");
+ } catch (InvalidStateException e) {
+ // expected ...
+ }
+ } else if (i == 1) {
+ // 2nd time: Save "/Cars" but keep the changes ...
+ assertConnectionsUsed(0);
+ cache.save(cars);
+ assertConnectionsUsed(2); // 1 to load children required by validation, 1
to perform save
+ }
+ // i=2 do nothing
+ }
+
+ // The affected nodes should now be stale ...
+ assertThat(sports.isStale(), is(true));
+ assertThat(utility.isStale(), is(true));
+ assertThat(cars.isStale(), is(false)); // not stale because it was unloaded
+ assertThat(cars.isLoaded(), is(false));
+ assertThat(root.isStale(), is(false)); // not touched during saves
+
+ // Now the state should reflect our changes, but we need to refind the nodes ...
+ sports = cache.findNodeWith(path("/Cars/Utility/Sports"));
+ assertConnectionsUsed(1);
+ utility = cache.findNodeWith(path("/Cars/Utility"));
+ assertNoMoreConnectionsUsed();
+
+ assertChildren(cars, "Hybrid", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150", "Sports");
+ assertThat(sports.getParent(), is(utility));
+ assertThat(utility.getParent(), is(cars));
+
+ // Now there should be no changes ...
+ assertNoChanges();
+
+ System.out.println(cache.root.getSnapshot(false));
+ }
+
+ @Test
+ public void shouldMoveBranchAndSaveAll() {
+ Node<Object, Object> sports =
cache.findNodeWith(path("/Cars/Sports"));
+ Node<Object, Object> utility =
cache.findNodeWith(path("/Cars/Utility"));
+ assertConnectionsUsed(1); // "Utility" was found because it is child of
"Cars" loaded when "Sports" was loaded
+
+ sports.moveTo(utility);
+ assertConnectionsUsed(1); // "Utility" was not fully loaded before
+ assertNoMoreConnectionsUsed();
+
+ Node<Object, Object> cars = cache.findNodeWith(path("/Cars"));
+ Node<Object, Object> root = cache.getRoot();
+
+ assertChildren(cars, "Hybrid", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150", "Sports");
+ assertThat(sports.getParent(), is(utility));
+ assertThat(utility.getParent(), is(cars));
+
+ // Ensure that the changes were recorded appropriately ...
+ assertThat(cars.isChanged(false), is(true));
+ assertThat(sports.isChanged(false), is(false));
+ assertThat(utility.isChanged(false), is(true));
+ assertThat(cache.hasPendingChanges(), is(true));
+ assertThat(cache.changeDependencies.size(), is(1));
+ assertThat(cache.changeDependencies.get(sports.getNodeId()).getMovedFrom(),
is(cars.getNodeId()));
+ assertThat(cache.operations.isExecuteRequired(), is(true));
+
+ // Save the changes ...
+ assertConnectionsUsed(0);
+ cache.save();
+ assertConnectionsUsed(2); // 1 to load children required by validation, 1 to
perform save
+
+ // The affected nodes should now be stale ...
+ assertThat(sports.isStale(), is(true));
+ assertThat(utility.isStale(), is(true));
+ assertThat(cars.isStale(), is(false)); // not stale because it was unloaded
+ assertThat(cars.isLoaded(), is(false));
+ assertThat(root.isStale(), is(false)); // not touched during saves
+
+ // Now the state should reflect our changes, but we need to refind the nodes ...
+ sports = cache.findNodeWith(path("/Cars/Utility/Sports"));
+ assertConnectionsUsed(1);
+ utility = cache.findNodeWith(path("/Cars/Utility"));
+ assertNoMoreConnectionsUsed();
+
+ assertChildren(cars, "Hybrid", "Luxury",
"Utility");
+ assertChildren(utility, "Land Rover LR2", "Land Rover LR3",
"Hummer H3", "Ford F-150", "Sports");
+ assertThat(sports.getParent(), is(utility));
+ assertThat(utility.getParent(), is(cars));
+
+ // Now there should be no changes ...
+ assertNoChanges();
+
+ // System.out.println(cache.root.getSnapshot(false));
+ }
+
+ @Test
+ public void shouldLoadSubgraphs() {
+ cache.setDepthForLoadingNodes(4);
+ Node<Object, Object> cars = cache.findNodeWith(path("/Cars")); //
loads the node and all parents
+ assertChildren(cars, "Hybrid", "Sports", "Luxury",
"Utility");
+ assertConnectionsUsed(1);
+
+ Node<Object, Object> sports =
cache.findNodeWith(path("/Cars/Sports"));
+ Node<Object, Object> g37 =
cache.findNodeWith(path("/Cars/Sports/Infiniti G37"));
+ Node<Object, Object> db9 = cache.findNodeWith(path("/Cars/Sports/Aston
Martin DB9"));
+ assertConnectionsUsed(0); // should have been loaded with sports subgraph
+
+ assertThat(sports.isLoaded(), is(true));
+ assertThat(g37.isLoaded(), is(true));
+ assertThat(db9.isLoaded(), is(true));
+ assertChildren(sports, "Aston Martin DB9", "Infiniti G37");
+ assertChildren(g37);
+ assertChildren(db9);
+
+ assertNoMoreConnectionsUsed();
+ assertNoChanges();
+ }
+
+ @Test
+ public void shouldMarkAsChangedWhenSettingProperties() {
+ Node<Object, Object> g37 =
cache.findNodeWith(path("/Cars/Sports/Infiniti G37"));
+ assertThat(g37.isChanged(false), is(false));
+
+ // Set the new property ...
+ Property newProperty = createProperty("something",
"value1");
+ g37.setProperty(newProperty, false, null);
+ assertThat(g37.isChanged(false), is(true));
+ assertThat(cache.getRoot().isChanged(true), is(true));
+
+ // Save the changes ...
+ cache.save();
+ }
+
+ @Test
+ public void shouldClearPropertyChangesWhenRefreshing() {
+ Node<Object, Object> g37 =
cache.findNodeWith(path("/Cars/Sports/Infiniti G37"));
+ assertThat(g37.isChanged(false), is(false));
+
+ // Set the new property ...
+ Property newProperty = createProperty("something",
"value1");
+ g37.setProperty(newProperty, false, null);
+ assertThat(g37.isChanged(false), is(true));
+ assertThat(cache.getRoot().isChanged(true), is(true));
+
+ // Refresh the changes ...
+ cache.refresh(g37, false);
+
+ assertThat(g37.isChanged(true), is(false));
+ assertThat(cache.getRoot().isChanged(true), is(false));
+ assertNoChanges();
+ }
+
+ protected void assertChildren( Node<Object, Object> node,
+ String... childNames ) {
+ assertThat(node.getChildrenCount(), is(childNames.length));
+ List<Path.Segment> segments = new LinkedList<Path.Segment>();
+ for (String childName : childNames) {
+ segments.add(pathFactory.createSegment(childName));
+ }
+ Iterator<Path.Segment> expectedIter = segments.iterator();
+ Iterator<Node<Object, Object>> actualIter =
node.getChildren().iterator();
+ while (expectedIter.hasNext() && actualIter.hasNext()) {
+ Node<Object, Object> actualNode = actualIter.next();
+ Path actualPath = actualNode.getPath();
+ Path.Segment actualSegment = actualPath.getLastSegment();
+ Path.Segment expectedSegment = expectedIter.next();
+ assertThat(actualSegment, is(expectedSegment));
+ }
+ assertThat(expectedIter.hasNext(), is(false));
+ assertThat(actualIter.hasNext(), is(false));
+ }
+
+ protected Name name( String name ) {
+ return context.getValueFactories().getNameFactory().create(name);
+ }
+
+ protected Path.Segment segment( String segment ) {
+ return context.getValueFactories().getPathFactory().createSegment(segment);
+ }
+
+ protected Path path( String path ) {
+ return context.getValueFactories().getPathFactory().create(path);
+ }
+
+ protected Property createProperty( String name,
+ Object... values ) {
+ return context.getPropertyFactory().create(name(name), values);
+ }
+
+ protected void assertChildrenNotLoaded( Node<Object, Object> node ) {
+ for (Node<Object, Object> child : node.getChildren()) {
+ assertThat(child.isLoaded(), is(false));
+ }
+ }
+
+ protected void assertNoChanges() {
+ assertThat(cache.hasPendingChanges(), is(false));
+ assertThat(cache.changeDependencies.isEmpty(), is(true));
+ assertThat(cache.operations.isExecuteRequired(), is(false));
+ }
+
+ protected void assertNoMoreConnectionsUsed() {
+ assertThat(numberOfConnections, is(0));
+ }
+
+ protected void assertConnectionsUsed( int number ) {
+ assertThat(numberOfConnections, is(number));
+ numberOfConnections = 0;
+ }
+
+}
Property changes on:
trunk/dna-graph/src/test/java/org/jboss/dna/graph/session/GraphCacheTest.java
___________________________________________________________________
Name: svn:keywords
+ Id Revision
Name: svn:eol-style
+ LF