[dna-commits] DNA SVN: r911 - in trunk: dna-graph/src/main/java/org/jboss/dna/graph and 10 other directories.

dna-commits at lists.jboss.org dna-commits at lists.jboss.org
Mon May 18 11:36:58 EDT 2009


Author: rhauch
Date: 2009-05-18 11:36:58 -0400 (Mon, 18 May 2009)
New Revision: 911

Added:
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjector.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedLexicon.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepository.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnection.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySource.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedWorkspace.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/GeneralProjector.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinMirrorRequestProcessor.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessor.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/MirrorProjector.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/NoMoreFederatedRequests.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjector.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectedNode.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projection.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectionParser.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projector.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholders.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/package-info.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryConnection.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryRequestProcessor.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractProjectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnectionTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorProjectionTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingOffsetProjectionTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRequestTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedWorkspaceTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorChannelTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/GeneralProjectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/MirrorProjectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionParserTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionPathRuleTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholdersTest.java
Modified:
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java
   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/Location.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CloneWorkspaceRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CompositeRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CopyBranchRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateNodeRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateWorkspaceRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/DeleteBranchRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/GetWorkspacesRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllChildrenRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllPropertiesRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBlockOfChildrenRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBranchRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNextBlockOfChildrenRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNodeRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadPropertyRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RemovePropertyRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RenameNodeRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/SetPropertyRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/UpdatePropertiesRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyNodeExistsRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyWorkspaceRequest.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java
   trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryConnectorReadableTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/AbstractConnectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/ReadableConnectorTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WorkspaceConnectorTest.java
   trunk/pom.xml
Log:
DNA-403 Federation connector does not behave properly in many situations

Implemented a new federation connector (in 'dna-graph') that addresses most if not all of the concerns listed in the issue.  Some changes were made to the requests (e.g., addition of DeleteChildrenRequest) and ExecutionContext and Location.  However, most of the changes were in the addition of the 'org.jboss.dna.graph.connector.federation' package and its mostly package-level classes.

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -42,6 +42,15 @@
     public static final Name CLASSPATH = new BasicName(Namespace.URI, "classpath");
     public static final Name NAMESPACES = new BasicName(Namespace.URI, "namespaces");
     public static final Name PROJECTION_RULES = new BasicName(Namespace.URI, "projectionRules");
+    public static final Name READ_ONLY = new BasicName(Namespace.URI, "readOnly");
     public static final Name TIME_TO_EXPIRE = new BasicName(Namespace.URI, "timeToExpire");
     public static final Name NAMESPACE_URI = new BasicName(Namespace.URI, "uri");
+
+    public static final Name WORKSPACES = new BasicName(Namespace.URI, "workspaces");
+    public static final Name SOURCE_NAME = new BasicName(Namespace.URI, "sourceName");
+    public static final Name WORKSPACE_NAME = new BasicName(Namespace.URI, "workspaceName");
+    public static final Name DEFAULT_WORKSPACE_NAME = new BasicName(Namespace.URI, "defaultWorkspaceName");
+    public static final Name PROJECTION = new BasicName(Namespace.URI, "projection");
+    public static final Name PROJECTIONS = new BasicName(Namespace.URI, "projections");
+
 }

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -42,6 +42,7 @@
 import org.jboss.dna.common.component.StandardClassLoaderFactory;
 import org.jboss.dna.common.util.CheckArg;
 import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.graph.connector.federation.FederatedLexicon;
 import org.jboss.dna.graph.mimetype.ExtensionBasedMimeTypeDetector;
 import org.jboss.dna.graph.mimetype.MimeTypeDetector;
 import org.jboss.dna.graph.property.NamespaceRegistry;
@@ -421,7 +422,7 @@
                                   CallbackHandler callbackHandler ) throws LoginException {
         LoginContext loginContext = new LoginContext(name, callbackHandler);
         loginContext.login();
-        
+
         return new ExecutionContext(this, loginContext);
     }
 
@@ -464,7 +465,7 @@
                                   CallbackHandler callbackHandler ) throws LoginException {
         LoginContext loginContext = new LoginContext(name, subject, callbackHandler);
         loginContext.login();
-        
+
         return new ExecutionContext(this, loginContext);
     }
 
@@ -499,6 +500,7 @@
         namespaceRegistry.register(JcrMixLexicon.Namespace.PREFIX, JcrMixLexicon.Namespace.URI);
         namespaceRegistry.register(JcrNtLexicon.Namespace.PREFIX, JcrNtLexicon.Namespace.URI);
         namespaceRegistry.register(DnaLexicon.Namespace.PREFIX, DnaLexicon.Namespace.URI);
+        namespaceRegistry.register(FederatedLexicon.Namespace.PREFIX, DnaLexicon.Namespace.URI);
         // namespaceRegistry.register("dnadtd", "http://www.jboss.org/dna/dtd/1.0");
         // namespaceRegistry.register("dnaxml", "http://www.jboss.org/dna/xml/1.0");
     }
@@ -521,9 +523,8 @@
         }
 
         /**
+         * {@inheritDoc}
          * 
-         * {@inheritDoc}
-         *
          * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
          */
         public void handle( Callback[] callbacks ) throws UnsupportedCallbackException, IOException {

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-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/Graph.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -986,7 +986,7 @@
                                                 getCurrentWorkspaceName(),
                                                 child,
                                                 EMPTY_PROPERTIES,
-                                                NodeConflictBehavior.DO_NOT_REPLACE).getActualLocationOfNode();
+                                                NodeConflictBehavior.UPDATE).getActualLocationOfNode();
         return new GetNodeOrReturnGraph(location);
     }
 
@@ -1009,7 +1009,7 @@
                                                 getCurrentWorkspaceName(),
                                                 child,
                                                 EMPTY_PROPERTIES,
-                                                NodeConflictBehavior.DO_NOT_REPLACE).getActualLocationOfNode();
+                                                NodeConflictBehavior.UPDATE).getActualLocationOfNode();
         return new GetNodeOrReturnGraph(location);
     }
 
@@ -1716,15 +1716,13 @@
                             }
 
                             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 ) {
@@ -5248,10 +5246,7 @@
         }
 
         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);
         }

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-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/GraphI18n.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -68,6 +68,7 @@
     public static I18n executedRequest;
     public static I18n closingRequestProcessor;
     public static I18n closedRequestProcessor;
+    public static I18n multipleErrorsWhileExecutingManyRequests;
     public static I18n multipleErrorsWhileExecutingRequests;
     public static I18n unsupportedRequestType;
     public static I18n unableToAddMoreRequestsToAlreadyExecutedBatch;
@@ -88,13 +89,33 @@
     public static I18n unableToFindRepositorySourceWithName;
 
     /* In-Memory Connector */
-    public static I18n inMemoryConnectorName;
     public static I18n inMemoryNodeDoesNotExist;
     public static I18n errorSerializingInMemoryCachePolicyInSource;
     public static I18n inMemoryConnectorRequestsMustHavePathOrUuid;
     public static I18n workspaceDoesNotExistInRepository;
     public static I18n workspaceAlreadyExistsInRepository;
 
+    /* Federation Connection */
+    public static I18n namePropertyIsRequiredForFederatedRepositorySource;
+    public static I18n propertyIsRequiredForFederatedRepositorySource;
+    public static I18n federatedRepositorySourceMustBeInitialized;
+    public static I18n errorReadingConfigurationForFederatedRepositorySource;
+    public static I18n errorAddingProjectionRuleParseMethod;
+    public static I18n requiredNodeDoesNotExistRelativeToNode;
+    public static I18n unableToObtainConnectionToFederatedSource;
+    public static I18n workspaceDoesNotExistInFederatedRepository;
+    public static I18n locationCannotBeProjectedIntoWorkspaceAndSource;
+    public static I18n unableToAddRequestToChannelThatIsDone;
+    public static I18n federatedSourceDoesNotSupportCreatingWorkspaces;
+    public static I18n federatedSourceDoesNotSupportCloningWorkspaces;
+    public static I18n federatedSourceDoesNotSupportDestroyingWorkspaces;
+    public static I18n unableToProjectSourceInformationIntoWorkspace;
+    public static I18n unableToCreateNodeUnderPlaceholder;
+    public static I18n unableToUpdatePlaceholder;
+    public static I18n unableToDeletePlaceholder;
+    public static I18n copyLimitedToBeWithinSingleSource;
+    public static I18n moveLimitedToBeWithinSingleSource;
+
     static {
         try {
             I18n.initialize(GraphI18n.class);

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/Location.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/Location.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/Location.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -45,7 +45,7 @@
  * @author Randall Hauch
  */
 @Immutable
-public abstract class Location implements Iterable<Property> {
+public abstract class Location implements Iterable<Property>, Comparable<Location> {
 
     /**
      * Simple shared iterator instance that is used when there are no properties.
@@ -432,6 +432,24 @@
     }
 
     /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo( Location that ) {
+        if (this == that) return 0;
+        if (this.hasPath() && that.hasPath()) {
+            return this.getPath().compareTo(that.getPath());
+        }
+        UUID thisUuid = this.getUuid();
+        UUID thatUuid = that.getUuid();
+        if (thisUuid != null && thatUuid != null) {
+            return thisUuid.compareTo(thatUuid);
+        }
+        return this.hashCode() - that.hashCode();
+    }
+
+    /**
      * Get the string form of the location.
      * 
      * @return the string

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjector.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjector.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjector.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,193 @@
+/*
+ * 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.connector.federation;
+
+import java.util.Iterator;
+import java.util.List;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.Path.Segment;
+
+/**
+ * A Projector for federated repository configurations that have a single mirrored projection (a projection that is a one-for-one
+ * mapping to a single source) and a single branch projection that maps a single path in the source to a single path in the
+ * federated repository.
+ * <p>
+ * For example, consider a federated repository that maps the "/" path in source1 into "/" in the federated repository, and the
+ * "/path/in/source" path into the "/federated/path" branch in the federated repository. Thus, anything at or below
+ * "/federated/path" goes into the (If this were the only projection, then a {@link MirrorProjector} could be used.)
+ */
+ at Immutable
+final class BranchedMirrorProjector extends ProjectorWithPlaceholders {
+
+    /**
+     * Attempt to create an instance of the {@link BranchedMirrorProjector} with the supplied projections using the supplied
+     * context.
+     * 
+     * @param context the context; may not be null
+     * @param projections the projections in the federated repository; may not be null
+     * @return the branched mirror projector, or null if the projections didn't match the criteria for such a projector
+     */
+    static BranchedMirrorProjector with( ExecutionContext context,
+                                         List<Projection> projections ) {
+        assert projections != null;
+        assert context != null;
+        if (projections.size() != 2) return null;
+        Projection first = projections.get(0);
+        Projection second = projections.get(1);
+        if (first.getRules().size() != 1) return null;
+        if (second.getRules().size() != 1) return null;
+        Rule firstRule = first.getRules().get(0);
+        Rule secondRule = second.getRules().get(0);
+        assert firstRule != null;
+        assert secondRule != null;
+        PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        List<Path> firstTopLevelPaths = first.getRules().get(0).getTopLevelPathsInRepository(pathFactory);
+        if (firstTopLevelPaths.size() != 1) return null;
+        List<Path> secondTopLevelPaths = second.getRules().get(0).getTopLevelPathsInRepository(pathFactory);
+        if (secondTopLevelPaths.size() != 1) return null;
+        // We're good, so create the instance ...
+        Path firstTopLevelPath = firstTopLevelPaths.get(0);
+        Path secondTopLevelPath = secondTopLevelPaths.get(0);
+        if (firstTopLevelPath.isRoot()) {
+            return new BranchedMirrorProjector(context, projections, first, second, secondTopLevelPath,
+                                               secondRule.getPathInSource(secondTopLevelPath, pathFactory));
+        }
+        return new BranchedMirrorProjector(context, projections, second, first, firstTopLevelPath,
+                                           firstRule.getPathInSource(firstTopLevelPath, pathFactory));
+    }
+
+    private final Projection mirrorProjection;
+    private final Projection branchProjection;
+    private final Path branchFederatedPath;
+    private final Path branchSourcePath;
+    private final boolean branchSourceUsesSamePath;
+
+    BranchedMirrorProjector( ExecutionContext context,
+                             List<Projection> projections,
+                             Projection mirrorProjection,
+                             Projection branchProjection,
+                             Path branchFederatedPath,
+                             Path branchSourcePath ) {
+        super(context, projections);
+        assert mirrorProjection != null;
+        assert branchProjection != null;
+        assert branchFederatedPath != null;
+        assert branchSourcePath != null;
+        this.mirrorProjection = mirrorProjection;
+        this.branchProjection = branchProjection;
+        this.branchFederatedPath = branchFederatedPath;
+        this.branchSourcePath = branchSourcePath;
+        this.branchSourceUsesSamePath = branchSourcePath.equals(branchFederatedPath);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation <i>always<i> returns a single {@link ProxyNode} for the location in the single projection.
+     * </p>
+     * 
+     * @see org.jboss.dna.graph.connector.federation.Projector#project(ExecutionContext, Location, boolean)
+     */
+    public ProjectedNode project( ExecutionContext context,
+                                  Location location,
+                                  boolean requiresUpdate ) {
+        if (location.hasPath()) {
+            Path path = location.getPath();
+            if (path.isRoot()) {
+                // It is a projection of the mirror's root and the placeholder for the branch root ...
+                if (requiresUpdate && mirrorProjection.isReadOnly()) return null;
+                ProxyNode result = new ProxyNode(mirrorProjection, location, location.with(branchSourcePath),
+                                                 branchSourceUsesSamePath);
+                result.add(isPlaceholder(location));
+                return result;
+            }
+            ProjectedNode onBranch = isOnBranch(path, location, context);
+            if (onBranch != null) {
+                if (requiresUpdate && branchProjection.isReadOnly()) return null;
+                return onBranch;
+            }
+            // Otherwise it is a proxy to the mirror only ...
+            if (requiresUpdate && mirrorProjection.isReadOnly()) return null;
+            return new ProxyNode(mirrorProjection, location, location, true);
+        }
+
+        // The location has no path and only identifier properties ...
+        if (requiresUpdate) {
+            if (branchProjection.isReadOnly()) {
+                // Can't update branch ...
+                if (mirrorProjection.isReadOnly()) return null;
+                return new ProxyNode(mirrorProjection, location, location, true); // no paths
+            }
+            if (mirrorProjection.isReadOnly()) {
+                // Can't update mirror ...
+                return new ProxyNode(branchProjection, location, location, branchSourceUsesSamePath); // no paths
+            }
+        }
+
+        // This is just a read, so create one projection for the mirror, and a second one for the branch.
+        ProjectedNode result = new ProxyNode(mirrorProjection, location, location, true);
+        result.add(new ProxyNode(branchProjection, location, location, branchSourceUsesSamePath)); // no paths
+        return result;
+    }
+
+    protected final ProjectedNode isOnBranch( Path federatedPath,
+                                              Location location,
+                                              ExecutionContext context ) {
+        Iterator<Segment> branchIter = branchFederatedPath.iterator();
+        Iterator<Segment> federIter = federatedPath.iterator();
+        // Look at the first path ...
+        if (branchIter.hasNext() && federIter.hasNext()) {
+            if (!branchIter.next().equals(federIter.next())) return null; // not on branch
+        }
+        // Otherwise, the federated path is on the branch, but how far ...
+        while (branchIter.hasNext() && federIter.hasNext()) {
+            if (!branchIter.next().equals(federIter.next())) {
+                // Didn't make it all the way along the branch ...
+                return null;
+            }
+        }
+        if (branchIter.hasNext()) {
+            // Didn't make it all the way down to the top of the branch, but it is a placeholder ...
+            return isPlaceholder(location);
+        }
+        // Otherwise it is within the brach ...
+        Location locationInSource = location;
+        if (!branchSourceUsesSamePath) {
+            // The source uses a different path ...
+            if (federIter.hasNext()) {
+                Path subpath = federatedPath.subpath(branchFederatedPath.size());
+                Path sourcePath = context.getValueFactories().getPathFactory().create(branchSourcePath, subpath);
+                locationInSource = location.with(sourcePath);
+            } else {
+                locationInSource = location.with(branchSourcePath);
+            }
+        }
+        return new ProxyNode(branchProjection, locationInSource, location, branchSourceUsesSamePath);
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjector.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Copied: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedLexicon.java (from rev 910, trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java)
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedLexicon.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedLexicon.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,42 @@
+/*
+ * 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.connector.federation;
+
+import org.jboss.dna.graph.DnaLexicon;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.basic.BasicName;
+
+/**
+ * @author Randall Hauch
+ */
+public class FederatedLexicon extends DnaLexicon {
+
+    public static final Name WORKSPACES = new BasicName(Namespace.URI, "workspaces");
+    public static final Name CACHE = new BasicName(Namespace.URI, "cache");
+    public static final Name PROJECTION = new BasicName(Namespace.URI, "projection");
+    public static final Name PROJECTIONS = new BasicName(Namespace.URI, "projections");
+    public static final Name SOURCE_NAME = new BasicName(Namespace.URI, "sourceName");
+    public static final Name WORKSPACE_NAME = new BasicName(Namespace.URI, "workspaceName");
+    public static final Name DEFAULT_WORKSPACE_NAME = new BasicName(Namespace.URI, "defaultWorkspaceName");
+}

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepository.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepository.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepository.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.RepositorySource;
+import org.jboss.dna.graph.request.InvalidWorkspaceException;
+
+/**
+ * The configuration of a {@link FederatedRepositorySource}.
+ */
+ at Immutable
+class FederatedRepository {
+
+    private final String sourceName;
+    private final CachePolicy defaultCachePolicy;
+    private final RepositoryConnectionFactory connectionFactory;
+    private final Map<String, FederatedWorkspace> workspacesByName;
+    private final FederatedWorkspace defaultWorkspace;
+    private final ExecutorService executor;
+
+    /**
+     * Construct a new instance of a configuration defining how the workspaces in a federated repository should project the
+     * content from one or more sources.
+     * 
+     * @param sourceName the name of the federated repository source; may not be null
+     * @param connectionFactory the factory for connections to the sources being federated; may not be null
+     * @param workspaces the workspaces that make up this federated repository; may not be null or empty
+     * @param defaultCachePolicy the default cache policy for the source, or null if there is no default caching policy for the
+     *        whole federated repository (each workspace may have its own)
+     * @param executor the {@link ExecutorService} that can be used to parallelize actions within this repository;may not be null
+     */
+    public FederatedRepository( String sourceName,
+                                RepositoryConnectionFactory connectionFactory,
+                                Iterable<FederatedWorkspace> workspaces,
+                                CachePolicy defaultCachePolicy,
+                                ExecutorService executor ) {
+        CheckArg.isNotNull(sourceName, "sourceName");
+        CheckArg.isNotNull(connectionFactory, "connectionFactory");
+        CheckArg.isNotNull(workspaces, "workspaces");
+        CheckArg.isNotNull(executor, "executor");
+        this.sourceName = sourceName;
+        this.connectionFactory = connectionFactory;
+        this.executor = executor;
+        this.defaultCachePolicy = defaultCachePolicy;
+        this.workspacesByName = new HashMap<String, FederatedWorkspace>();
+        FederatedWorkspace defaultWorkspace = null;
+        for (FederatedWorkspace workspace : workspaces) {
+            if (defaultWorkspace == null) defaultWorkspace = workspace;
+            this.workspacesByName.put(workspace.getName(), workspace);
+        }
+        this.defaultWorkspace = defaultWorkspace;
+        assert this.defaultWorkspace != null;
+        assert this.workspacesByName.size() > 0;
+    }
+
+    /**
+     * Get the name of the {@link RepositorySource} that owns this configuration.
+     * 
+     * @return the source's name; never null
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * Get the {@link RepositoryConnectionFactory factory for connections} to the sources that are being federated by this
+     * repository.
+     * 
+     * @return the connection factory for the sources; never null
+     */
+    public RepositoryConnectionFactory getConnectionFactory() {
+        return connectionFactory;
+    }
+
+    /**
+     * Get the {@link FederatedWorkspace workspace} information, given its name.
+     * 
+     * @param name the name of the workspace, or null if the default workspace should be returned
+     * @return the workspace
+     * @throws InvalidWorkspaceException if the specified workspace does not exist
+     */
+    public FederatedWorkspace getWorkspace( String name ) {
+        if (name == null) {
+            assert defaultWorkspace != null;
+            return defaultWorkspace;
+        }
+        FederatedWorkspace workspace = workspacesByName.get(name);
+        if (workspace == null) {
+            String msg = GraphI18n.workspaceDoesNotExistInFederatedRepository.text(name, getSourceName());
+            throw new InvalidWorkspaceException(msg);
+        }
+        return workspace;
+    }
+
+    /**
+     * Get the names of the available workspaces.
+     * 
+     * @return the unmodifiable copy of the workspace names; never null
+     */
+    Set<String> getWorkspaceNames() {
+        return Collections.unmodifiableSet(new HashSet<String>(workspacesByName.keySet()));
+    }
+
+    /**
+     * Get the default cache policy that used for the whole repository. Note that the repository may or may not have a default
+     * caching policy, and each {@link FederatedWorkspace workspace} may have its own {@link FederatedWorkspace#getCachePolicy()
+     * caching policy}.
+     * 
+     * @return the default cache policy for the repository, used if/when the workspace(s) don't have their own caching policy or
+     *         when the source content does not specify the caching policy; may be null
+     */
+    public CachePolicy getDefaultCachePolicy() {
+        return defaultCachePolicy;
+    }
+
+    /**
+     * Get the executor for this repository. This executor can be used to process tasks.
+     * 
+     * @return the executor; may not be null
+     */
+    public ExecutorService getExecutor() {
+        return executor;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "Federated repository \"" + getSourceName() + "\" with workspaces " + workspacesByName.keySet();
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepository.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnection.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnection.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnection.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import javax.transaction.xa.XAResource;
+import org.jboss.dna.common.statistic.Stopwatch;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositorySourceException;
+import org.jboss.dna.graph.connector.RepositorySourceListener;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.request.CompositeRequest;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * This {@link RepositoryConnection} implementation executes {@link Request requests} against the federated repository by
+ * projecting them into requests against the underlying sources that are being federated.
+ * <p>
+ * One important design of the connector framework is that requests can be submitted in {@link CompositeRequest batch} to a
+ * {@link RepositoryConnection connection}, which may perform them more efficiently than if each request was submitted one at a
+ * time. This connector design maintains this feature by projecting the incoming requests into requests against each source, then
+ * submitting the batch of projected requests to each source, and then transforming the results of the projected requests back
+ * into original requests.
+ * </p>
+ * <p>
+ * This is accomplished using a three-step process:
+ * <ol>
+ * <li><strong>Step 1:</strong> Process the incoming requests and for each generate the appropriate request(s) against the sources
+ * (dictated by the {@link FederatedWorkspace workspace's} {@link FederatedWorkspace#getProjections() projections}). These
+ * "projected requests" are then enqueued for each source.</li>
+ * <li><strong>Step 2:</strong> Submit each batch of projected requests to the appropriate source, in parallel where possible.
+ * Note that the requests are still ordered correctly for each source.</li>
+ * <li><strong>Step 3:</strong> Accumulate the results for the incoming requests by post-processing the projected requests and
+ * transforming the source-specific results back into the federated workspace (again, using the workspace's projections).</li>
+ * </ol>
+ * </p>
+ * <p>
+ * This process is a form of the <i>fork-join</i> divide-and-conquer algorithm, which involves splitting a problem into smaller
+ * parts, forking new subtasks to execute each smaller part, joining on the subtasks (waiting until all have finished), and then
+ * composing the results. Technically, Step 2 performs the fork and join operations, but this class uses {@link RequestProcessor}
+ * implementations to do Step 1 and 3 (called {@link ForkRequestProcessor} and {@link JoinRequestProcessor}, respectively).
+ * </p>
+ */
+class FederatedRepositoryConnection implements RepositoryConnection {
+
+    private final FederatedRepository repository;
+    private final Stopwatch stopwatch;
+    private final Logger logger;
+
+    FederatedRepositoryConnection( FederatedRepository repository ) {
+        this.repository = repository;
+        this.logger = Logger.getLogger(getClass());
+        this.stopwatch = logger.isTraceEnabled() ? new Stopwatch() : null;
+    }
+
+    /**
+     * The federated repository that created this connection.
+     * 
+     * @return repository
+     */
+    FederatedRepository getRepository() {
+        return repository;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getDefaultCachePolicy()
+     */
+    public CachePolicy getDefaultCachePolicy() {
+        return repository.getDefaultCachePolicy(); // may be null
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getSourceName()
+     */
+    public String getSourceName() {
+        return repository.getSourceName();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getXAResource()
+     */
+    public XAResource getXAResource() {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#ping(long, java.util.concurrent.TimeUnit)
+     */
+    public boolean ping( long time,
+                         TimeUnit unit ) {
+        return true;
+    }
+
+    protected boolean shouldProcessSynchronously( Request request ) {
+        if (request instanceof CompositeRequest) {
+            CompositeRequest composite = (CompositeRequest)request;
+            if (composite.size() == 1) return true;
+            return false;
+        }
+        // Otherwise, its just a single request ...
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#execute(org.jboss.dna.graph.ExecutionContext,
+     *      org.jboss.dna.graph.request.Request)
+     */
+    public void execute( ExecutionContext context,
+                         final Request request ) throws RepositorySourceException {
+        // Compute the current time ...
+        DateTime nowInUtc = context.getValueFactories().getDateFactory().createUtc();
+
+        // Figure out whether we should asynchronously do the forking ...
+        boolean synchronousStep1 = shouldProcessSynchronously(request);
+        final boolean awaitAllSubtasks = false;
+
+        // Prepare for trace-level logging ...
+        if (stopwatch != null) stopwatch.start();
+
+        boolean abort = false;
+        try {
+            // ----------------------------------------------------------------------------------------------------
+            // Step 1: Fork the submitted requests into source-specific requests...
+            // ----------------------------------------------------------------------------------------------------
+            // This forks a subtask for each source, as soon as the first source-specific request for a source
+            // is generated. Each source's "execute(ExecutionContext,Request)" is called only once (a queue is
+            // used so that the source can begin processing the requests before all the requests have been
+            // computed and submitted to the subtask). Thus, it's possible (and likely) that this thread
+            // and subtask threads are executed in parallel.
+            final Queue<FederatedRequest> requests = awaitAllSubtasks ? new LinkedList<FederatedRequest>() : new LinkedBlockingQueue<FederatedRequest>();
+            final ForkRequestProcessor fork = new ForkRequestProcessor(repository, context, nowInUtc, requests);
+            if (synchronousStep1) {
+                // Execute the forking process in this thread ...
+                try {
+                    fork.process(request);
+                } finally {
+                    fork.close();
+                }
+                if (!awaitAllSubtasks) requests.add(new NoMoreFederatedRequests());
+                // At this point, all submitted requests have been processed/forked, so we can continue with
+                // the join process, starting with the first submitted request. Note that the subtasks may
+                // still be executing, but as the join process operates on a forked request, it will wait
+                // until all forked requests have completed. Hopefully, in most situations, the subtasks
+                // have enough of a lead that the join process never has to wait.
+            } else {
+                // Submit the forking process for execution in a separate thread ...
+                repository.getExecutor().submit(new Runnable() {
+                    public void run() {
+                        try {
+                            fork.process(request);
+                        } finally {
+                            fork.close();
+                        }
+                        if (!awaitAllSubtasks) requests.add(new NoMoreFederatedRequests());
+                    }
+                });
+
+                // At this point, the forking process is being run by a thread owned by the Executor. We'll still
+                // continue with the join process, starting with the first submitted request. Note that it is
+                // likely that the subtasks are still running in threads owned by the Executor.
+            }
+
+            if (awaitAllSubtasks) {
+                // Await until all subtasks have completed ...
+                fork.await();
+            }
+
+            // ----------------------------------------------------------------------------------------------------
+            // Step 2: Join the results of the source-specific (forked) requests back into the submitted requests
+            // ----------------------------------------------------------------------------------------------------
+            JoinRequestProcessor join = new JoinRequestProcessor(repository, context, nowInUtc);
+            try {
+                if (awaitAllSubtasks) {
+                    join.process(requests);
+                } else {
+                    join.process((BlockingQueue<FederatedRequest>)requests);
+                }
+            } catch (RuntimeException e) {
+                abort = true;
+                throw e;
+            } finally {
+                join.close();
+            }
+        } catch (InterruptedException e) {
+            abort = true;
+            request.setError(e);
+        } catch (ExecutionException e) {
+            abort = true;
+            request.setError(e);
+        } catch (CancellationException e) {
+            abort = true;
+            request.cancel();
+            // do nothing else
+        } catch (RuntimeException e) {
+            abort = true;
+            throw e;
+        } finally {
+            if (stopwatch != null) stopwatch.stop();
+            if (abort) {
+                // Rollback the transaction (if there is one) ...
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#close()
+     */
+    public void close() {
+        if (stopwatch != null) {
+            logger.trace("Processing federated requests:\n" + stopwatch.getDetailedStatistics());
+        }
+        // do nothing else, since we don't currently hold any state
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#setListener(org.jboss.dna.graph.connector.RepositorySourceListener)
+     */
+    public void setListener( RepositorySourceListener listener ) {
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnection.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySource.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySource.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySource.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,588 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.RefAddr;
+import javax.naming.Reference;
+import javax.naming.StringRefAddr;
+import javax.naming.spi.ObjectFactory;
+import net.jcip.annotations.GuardedBy;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.graph.DnaLexicon;
+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.Node;
+import org.jboss.dna.graph.Subgraph;
+import org.jboss.dna.graph.SubgraphNode;
+import org.jboss.dna.graph.cache.BasicCachePolicy;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.RepositoryContext;
+import org.jboss.dna.graph.connector.RepositorySource;
+import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
+import org.jboss.dna.graph.connector.RepositorySourceException;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+
+/**
+ * 
+ */
+public class FederatedRepositorySource implements RepositorySource, ObjectFactory {
+
+    /**
+     * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
+     */
+    public static final int DEFAULT_RETRY_LIMIT = 0;
+
+    /**
+     * The default path to the node defining the configuration for a source.
+     */
+    public static final String DEFAULT_CONFIGURATION_PATH = "/";
+
+    protected static final String SOURCE_NAME = "sourceName";
+    protected static final String RETRY_LIMIT = "retryLimit";
+    protected static final String CONFIGURATION_SOURCE_NAME = "configurationSourceName";
+    protected static final String CONFIGURATION_PATH = "configurationPath";
+    protected static final String CONFIGURATION_WORKSPACE_NAME = "configurationWorkspaceName";
+
+    private static final long serialVersionUID = 1L;
+
+    private volatile String name;
+    private volatile int retryLimit;
+    private volatile String configurationSourceName;
+    private volatile String configurationWorkspaceName;
+    private volatile String configurationPath = DEFAULT_CONFIGURATION_PATH;
+    private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(true, true, false, false, true);
+    private volatile transient FederatedRepository configuration;
+    private volatile transient RepositoryContext context;
+
+    /**
+     * Construct a new instance of a {@link RepositorySource} for a federated repository.
+     */
+    public FederatedRepositorySource() {
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#getName()
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name Sets name to the specified value.
+     */
+    public synchronized void setName( String name ) {
+        if (this.name == name || this.name != null && this.name.equals(name)) return; // unchanged
+        this.name = name;
+        changeConfiguration();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
+     */
+    public int getRetryLimit() {
+        return retryLimit;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
+     */
+    public synchronized void setRetryLimit( int limit ) {
+        retryLimit = limit < 0 ? 0 : limit;
+        changeConfiguration();
+    }
+
+    /**
+     * Get the name of the {@link RepositorySource} that should be used to create the {@link FederatedRepository federated
+     * repository configuration} as the configuration repository.
+     * <p>
+     * This is a required property.
+     * </p>
+     * 
+     * @return the name of the {@link RepositorySource} instance that should be used for the configuration
+     * @see #setConfigurationSourceName(String)
+     * @see #getConfigurationWorkspaceName()
+     * @see #getConfigurationPath()
+     */
+    public String getConfigurationSourceName() {
+        return configurationSourceName;
+    }
+
+    /**
+     * Set the name of the {@link RepositorySource} that should be used to create the {@link FederatedRepository federated
+     * repository configuration} as the configuration repository. The instance will be retrieved from the
+     * {@link RepositoryConnectionFactory} instance inside the {@link RepositoryContext#getRepositoryConnectionFactory()
+     * repository context} supplied during {@link RepositorySource#initialize(RepositoryContext) initialization}.
+     * <p>
+     * This is a required property.
+     * </p>
+     * 
+     * @param sourceName the name of the {@link RepositorySource} instance that should be used for the configuration, or null if
+     *        the federated repository instance is to be found in JNDI
+     * @see #getConfigurationSourceName()
+     * @see #setConfigurationPath(String)
+     * @see #setConfigurationWorkspaceName(String)
+     */
+    public synchronized void setConfigurationSourceName( String sourceName ) {
+        if (this.configurationSourceName == sourceName || this.configurationSourceName != null
+            && this.configurationSourceName.equals(sourceName)) return; // unchanged
+        this.configurationSourceName = sourceName;
+        changeConfiguration();
+    }
+
+    /**
+     * Get the name of the workspace in the {@link #getConfigurationSourceName() source} containing the configuration content for
+     * this source. If this workspace name is null, the default workspace as defined by that source will be used.
+     * 
+     * @return the name of the configuration workspace, or null if the default workspace for the
+     *         {@link #getConfigurationSourceName() configuration source} should be used
+     * @see #getConfigurationSourceName()
+     * @see #setConfigurationWorkspaceName(String)
+     * @see #getConfigurationPath()
+     */
+    public String getConfigurationWorkspaceName() {
+        return configurationWorkspaceName;
+    }
+
+    /**
+     * Set the name of the workspace in the {@link #getConfigurationSourceName() source} containing the configuration content for
+     * this source. If this workspace name is null, the default workspace as defined by that source will be used.
+     * 
+     * @param workspaceName the name of the configuration workspace, or null if the default workspace for the
+     *        {@link #getConfigurationSourceName() configuration source} should be used
+     * @see #setConfigurationSourceName(String)
+     * @see #setConfigurationPath(String)
+     * @see #getConfigurationWorkspaceName()
+     */
+    public synchronized void setConfigurationWorkspaceName( String workspaceName ) {
+        if (this.configurationWorkspaceName == workspaceName || this.configurationWorkspaceName != null
+            && this.configurationWorkspaceName.equals(workspaceName)) return; // unchanged
+        this.configurationWorkspaceName = workspaceName;
+        changeConfiguration();
+    }
+
+    /**
+     * Get the path in the {@link #getConfigurationWorkspaceName() workspace} of the {@link #getConfigurationSourceName()
+     * configuration source} where this source can find the content defining its configuration.
+     * <p>
+     * This is a required property.
+     * </p>
+     * 
+     * @return the string array of projection rules, or null if the projection rules haven't yet been set or if the federated
+     *         repository instance is to be found in JNDI
+     * @see #getConfigurationSourceName()
+     * @see #getConfigurationWorkspaceName()
+     * @see #setConfigurationPath(String)
+     */
+    public String getConfigurationPath() {
+        return configurationPath;
+    }
+
+    /**
+     * Set the path in the {@link #getConfigurationWorkspaceName() workspace} of the {@link #getConfigurationSourceName()
+     * configuration source} where this source can find the content defining its configuration.
+     * <p>
+     * This is a required property.
+     * </p>
+     * 
+     * @param pathInSourceToConfigurationRoot the path within the configuration source to the node that should be the root of the
+     *        configuration information, or null if the {@link #DEFAULT_CONFIGURATION_PATH default path} should be used
+     * @see #getConfigurationPath()
+     * @see #setConfigurationSourceName(String)
+     * @see #setConfigurationWorkspaceName(String)
+     */
+    public void setConfigurationPath( String pathInSourceToConfigurationRoot ) {
+        if (this.configurationPath == pathInSourceToConfigurationRoot || this.configurationPath != null
+            && this.configurationPath.equals(pathInSourceToConfigurationRoot)) return;
+        String path = pathInSourceToConfigurationRoot != null ? pathInSourceToConfigurationRoot : DEFAULT_CONFIGURATION_PATH;
+        // Ensure one leading slash and one trailing slashes ...
+        this.configurationPath = path = ("/" + path).replaceAll("^/+", "/").replaceAll("/+$", "") + "/";
+        changeConfiguration();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
+     */
+    public RepositorySourceCapabilities getCapabilities() {
+        return capabilities;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
+     */
+    public synchronized void initialize( RepositoryContext context ) throws RepositorySourceException {
+        this.context = context;
+        changeConfiguration();
+    }
+
+    /**
+     * Get the repository context that was used to {@link #initialize(RepositoryContext) initialize} this source.
+     * 
+     * @return the context, or null if the source was not yet {@link #initialize(RepositoryContext) initialized}
+     */
+    /*package*/RepositoryContext getRepositoryContext() {
+        return context;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
+     */
+    public RepositoryConnection getConnection() throws RepositorySourceException {
+        FederatedRepository config = this.configuration;
+        if (config == null) {
+            synchronized (this) {
+                if (this.configuration == null) {
+                    // Check all the properties of this source ...
+                    String name = getName();
+                    if (name == null) {
+                        I18n msg = GraphI18n.namePropertyIsRequiredForFederatedRepositorySource;
+                        throw new RepositorySourceException(getName(), msg.text("name"));
+                    }
+                    RepositoryContext repositoryContext = getRepositoryContext();
+                    if (repositoryContext == null) {
+                        I18n msg = GraphI18n.federatedRepositorySourceMustBeInitialized;
+                        throw new RepositorySourceException(getName(), msg.text("name", name));
+                    }
+                    String configSource = getConfigurationSourceName();
+                    String configWorkspace = getConfigurationWorkspaceName();
+                    String configPath = getConfigurationPath();
+                    if (configSource == null) {
+                        I18n msg = GraphI18n.propertyIsRequiredForFederatedRepositorySource;
+                        throw new RepositorySourceException(getName(), msg.text("configuration source name", name));
+                    }
+
+                    // Load the configuration ...
+                    this.configuration = loadRepository(name, repositoryContext, configSource, configWorkspace, configPath);
+                }
+                config = this.configuration;
+            }
+        }
+        return new FederatedRepositoryConnection(config);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see javax.naming.Referenceable#getReference()
+     */
+    public Reference getReference() {
+        String className = getClass().getName();
+        String factoryClassName = this.getClass().getName();
+        Reference ref = new Reference(className, factoryClassName, null);
+
+        ref.add(new StringRefAddr(SOURCE_NAME, getName()));
+        ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
+        ref.add(new StringRefAddr(CONFIGURATION_SOURCE_NAME, getConfigurationSourceName()));
+        ref.add(new StringRefAddr(CONFIGURATION_WORKSPACE_NAME, getConfigurationWorkspaceName()));
+        ref.add(new StringRefAddr(CONFIGURATION_PATH, getConfigurationPath()));
+        return ref;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context,
+     *      java.util.Hashtable)
+     */
+    public Object getObjectInstance( Object obj,
+                                     Name name,
+                                     Context nameCtx,
+                                     Hashtable<?, ?> environment ) throws Exception {
+        if (obj instanceof Reference) {
+            Map<String, String> values = new HashMap<String, String>();
+            Reference ref = (Reference)obj;
+            Enumeration<?> en = ref.getAll();
+            while (en.hasMoreElements()) {
+                RefAddr subref = (RefAddr)en.nextElement();
+                if (subref instanceof StringRefAddr) {
+                    String key = subref.getType();
+                    Object value = subref.getContent();
+                    if (value != null) values.put(key, value.toString());
+                }
+            }
+            String sourceName = values.get(SOURCE_NAME);
+            String retryLimit = values.get(RETRY_LIMIT);
+            String configSourceName = values.get(CONFIGURATION_SOURCE_NAME);
+            String configSourceWorkspace = values.get(CONFIGURATION_WORKSPACE_NAME);
+            String configSourcePath = values.get(CONFIGURATION_PATH);
+
+            // Create the source instance ...
+            FederatedRepositorySource source = new FederatedRepositorySource();
+            if (sourceName != null) source.setName(sourceName);
+            if (configSourceName != null) source.setConfigurationSourceName(configSourceName);
+            if (configSourceWorkspace != null) source.setConfigurationWorkspaceName(configSourceWorkspace);
+            if (configSourcePath != null) source.setConfigurationPath(configSourcePath);
+            if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
+            return source;
+        }
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(getName());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof FederatedRepositorySource) {
+            FederatedRepositorySource that = (FederatedRepositorySource)obj;
+            // The source name must match
+            if (this.getName() == null) {
+                if (that.getName() != null) return false;
+            } else {
+                if (!this.getName().equals(that.getName())) return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Mark the current configuration (if there is one) as being invalid.
+     */
+    @GuardedBy( "this" )
+    protected void changeConfiguration() {
+        this.configuration = null;
+    }
+
+    /**
+     * Utility to load the current configuration for this source from the {@link #getConfigurationSourceName() configuration
+     * repository}. This method may only be called after the source is {@link #initialize(RepositoryContext) initialized}.
+     * 
+     * @param name the name of the source; may not be null
+     * @param repositoryContext the repository context; may not be null
+     * @param configSource the name of the configuration source; may not be null
+     * @param configWorkspace the name of the workspace in the configuration source, or null if the configuration source's default
+     *        workspace should be used
+     * @param configPath the path to the node in the configuration workspace that defines the source; may not be null
+     * @return the configuration; never null
+     * @throws RepositorySourceException if there is a problem with the configuration
+     */
+    protected FederatedRepository loadRepository( String name,
+                                                  RepositoryContext repositoryContext,
+                                                  String configSource,
+                                                  String configWorkspace,
+                                                  String configPath ) throws RepositorySourceException {
+        // All the required properties have been set ...
+        ExecutionContext executionContext = repositoryContext.getExecutionContext();
+        RepositoryConnectionFactory connectionFactory = repositoryContext.getRepositoryConnectionFactory();
+        ValueFactories valueFactories = executionContext.getValueFactories();
+        ValueFactory<String> strings = valueFactories.getStringFactory();
+        ValueFactory<Long> longs = valueFactories.getLongFactory();
+        ProjectionParser projectionParser = ProjectionParser.getInstance();
+        NamespaceRegistry registry = executionContext.getNamespaceRegistry();
+
+        try {
+            Graph config = Graph.create(configSource, connectionFactory, executionContext);
+            if (configWorkspace != null) {
+                configWorkspace = config.useWorkspace(configWorkspace).getName();
+            } else {
+                configWorkspace = config.getCurrentWorkspaceName();
+            }
+
+            // Read the configuration for the federated repository:
+            // Level 1: the node representing the federated repository
+            // Level 2: the "dna:workspaces" node
+            // Level 3: a node for each workspace in the federated repository
+            // Level 4: the "dna:projections" nodes
+            // Level 5: a node below "dna:projections" for each projection, with properties for the source name,
+            // workspace name, cache expiration time, and projection rules
+            Subgraph repositories = config.getSubgraphOfDepth(5).at(configPath);
+
+            // Get the name of the default workspace ...
+            String defaultWorkspaceName = null;
+            Property defaultWorkspaceNameProperty = repositories.getRoot().getProperty(DnaLexicon.DEFAULT_WORKSPACE_NAME);
+            if (defaultWorkspaceNameProperty != null) {
+                // Set the name using the property if there is one ...
+                defaultWorkspaceName = strings.create(defaultWorkspaceNameProperty.getFirstValue());
+            }
+
+            // Get the default expiration time for the repository ...
+            CachePolicy defaultCachePolicy = null;
+            Property timeToExpire = repositories.getRoot().getProperty(DnaLexicon.TIME_TO_EXPIRE);
+            if (timeToExpire != null && !timeToExpire.isEmpty()) {
+                long timeToCacheInMillis = longs.create(timeToExpire.getFirstValue());
+                defaultCachePolicy = new BasicCachePolicy(timeToCacheInMillis, TimeUnit.MILLISECONDS).getUnmodifiable();
+            }
+
+            // Level 2: The "dna:workspaces" node ...
+            Node workspacesNode = repositories.getNode(DnaLexicon.WORKSPACES);
+            if (workspacesNode == null) {
+                I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
+                throw new RepositorySourceException(msg.text(DnaLexicon.WORKSPACES.getString(registry),
+                                                             repositories.getLocation().getPath().getString(registry),
+                                                             configWorkspace,
+                                                             configSource));
+            }
+
+            // Level 3: The workspace nodes ...
+            LinkedList<FederatedWorkspace> workspaces = new LinkedList<FederatedWorkspace>();
+            for (Location workspace : workspacesNode) {
+
+                // Get the name of the workspace ...
+                String workspaceName = null;
+                SubgraphNode workspaceNode = repositories.getNode(workspace);
+                Property workspaceNameProperty = workspaceNode.getProperty(DnaLexicon.WORKSPACE_NAME);
+                if (workspaceNameProperty != null) {
+                    // Set the name using the property if there is one ...
+                    workspaceName = strings.create(workspaceNameProperty.getFirstValue());
+                }
+                if (workspaceName == null) {
+                    // Otherwise, set the name using the local name of the workspace node ...
+                    workspaceName = workspace.getPath().getLastSegment().getName().getLocalName();
+                }
+
+                // Level 4: the "dna:projections" node ...
+                Node projectionsNode = workspaceNode.getNode(DnaLexicon.PROJECTIONS);
+                if (projectionsNode == null) {
+                    I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
+                    throw new RepositorySourceException(getName(), msg.text(DnaLexicon.PROJECTIONS.getString(registry),
+                                                                            workspaceNode.getLocation()
+                                                                                         .getPath()
+                                                                                         .getString(registry),
+                                                                            configWorkspace,
+                                                                            configSource));
+                }
+
+                // Level 5: the projection nodes ...
+                List<Projection> sourceProjections = new LinkedList<Projection>();
+                for (Location projection : projectionsNode) {
+                    Node projectionNode = repositories.getNode(projection);
+                    sourceProjections.add(createProjection(executionContext, projectionParser, projectionNode));
+                }
+
+                // Create the federated workspace configuration ...
+                FederatedWorkspace space = new FederatedWorkspace(repositoryContext, name, workspaceName, sourceProjections,
+                                                                  defaultCachePolicy);
+                if (workspaceName.equals(defaultWorkspaceName)) {
+                    workspaces.addFirst(space);
+                } else {
+                    workspaces.add(space);
+                }
+            }
+
+            // Create the ExecutorService ...
+            ExecutorService executor = Executors.newCachedThreadPool();
+
+            return new FederatedRepository(name, connectionFactory, workspaces, defaultCachePolicy, executor);
+        } catch (RepositorySourceException t) {
+            throw t; // rethrow
+        } catch (Throwable t) {
+            I18n msg = GraphI18n.errorReadingConfigurationForFederatedRepositorySource;
+            throw new RepositorySourceException(getName(), msg.text(name, configSource, configWorkspace, configPath), t);
+        }
+    }
+
+    /**
+     * Instantiate the {@link Projection} described by the supplied properties.
+     * 
+     * @param context the execution context that should be used to read the configuration; may not be null
+     * @param projectionParser the projection rule parser that should be used; may not be null
+     * @param node the node where these properties were found; never null
+     * @return the region instance, or null if it could not be created
+     */
+    protected Projection createProjection( ExecutionContext context,
+                                           ProjectionParser projectionParser,
+                                           Node node ) {
+        ValueFactory<String> strings = context.getValueFactories().getStringFactory();
+
+        Path path = node.getLocation().getPath();
+
+        // Get the source name from the local name of the node ...
+        String sourceName = path.getLastSegment().getName().getLocalName();
+        Property sourceNameProperty = node.getProperty(DnaLexicon.SOURCE_NAME);
+        if (sourceNameProperty != null && !sourceNameProperty.isEmpty()) {
+            // There is a "dna:sourceName" property, so use this instead ...
+            sourceName = strings.create(sourceNameProperty.getFirstValue());
+        }
+        assert sourceName != null;
+
+        // Get the workspace name ...
+        String workspaceName = null;
+        Property workspaceNameProperty = node.getProperty(DnaLexicon.WORKSPACE_NAME);
+        if (workspaceNameProperty != null && !workspaceNameProperty.isEmpty()) {
+            // There is a "dna:workspaceName" property, so use this instead ...
+            workspaceName = strings.create(workspaceNameProperty.getFirstValue());
+        }
+
+        // Get the projection rules ...
+        Projection.Rule[] projectionRules = null;
+        Property projectionRulesProperty = node.getProperty(DnaLexicon.PROJECTION_RULES);
+        if (projectionRulesProperty != null && !projectionRulesProperty.isEmpty()) {
+            String[] projectionRuleStrs = strings.create(projectionRulesProperty.getValuesAsArray());
+            if (projectionRuleStrs != null && projectionRuleStrs.length != 0) {
+                projectionRules = projectionParser.rulesFromStrings(context, projectionRuleStrs);
+            }
+        }
+
+        // Is this projection read-only?
+        boolean readOnly = false;
+        Property readOnlyProperty = node.getProperty(DnaLexicon.READ_ONLY);
+        if (readOnlyProperty != null && !readOnlyProperty.isEmpty()) {
+            readOnly = context.getValueFactories().getBooleanFactory().create(readOnlyProperty.getFirstValue());
+        }
+
+        return new Projection(sourceName, workspaceName, readOnly, projectionRules);
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySource.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRequest.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.concurrent.CountDownLatch;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.graph.request.Request;
+
+/**
+ * A wrapper for a request submitted to the federated repository, and the corresponding source-specific {@link ProjectedRequest
+ * projected requests}.
+ */
+ at NotThreadSafe
+class FederatedRequest {
+    private final Request original;
+    private CountDownLatch forkLatch;
+    private int incompleteCount;
+    private ProjectedRequest first;
+
+    FederatedRequest( Request original ) {
+        this.original = original;
+    }
+
+    public Request original() {
+        return original;
+    }
+
+    public final FederatedRequest add( Request request,
+                                       boolean isSameLocationAsOriginal,
+                                       boolean isComplete,
+                                       Projection projection,
+                                       Projection secondProjection ) {
+        if (!isComplete) ++incompleteCount;
+        if (first == null) {
+            if (isSameLocationAsOriginal) {
+                first = new MirrorRequest(request, isComplete, projection, secondProjection);
+            } else {
+                first = new ProjectedRequest(request, isComplete, projection, secondProjection);
+            }
+        } else {
+            first.addNext(request, isComplete, projection);
+        }
+        return this;
+    }
+
+    public final FederatedRequest add( Request request,
+                                       boolean isSameLocationAsOriginal,
+                                       boolean isComplete,
+                                       Projection projection ) {
+        return add(request, isSameLocationAsOriginal, isComplete, projection, null);
+    }
+
+    public void freeze() {
+        if (incompleteCount > 0 && forkLatch == null) {
+            forkLatch = new CountDownLatch(incompleteCount);
+        }
+    }
+
+    public ProjectedRequest getFirstProjectedRequest() {
+        return first;
+    }
+
+    public boolean hasIncompleteRequests() {
+        return incompleteCount != 0;
+    }
+
+    public CountDownLatch getLatch() {
+        return forkLatch;
+    }
+
+    public void await() throws InterruptedException {
+        if (forkLatch != null) forkLatch.await();
+    }
+
+    class ProjectedRequest {
+        private final Projection projection;
+        private final Projection projection2;
+        private final Request request;
+        private final boolean isComplete;
+        private ProjectedRequest next;
+
+        protected ProjectedRequest( Request request,
+                                    boolean isComplete,
+                                    Projection projection,
+                                    Projection secondProjection ) {
+            this.projection = projection;
+            this.request = request;
+            this.isComplete = isComplete;
+            this.projection2 = secondProjection;
+        }
+
+        public Projection getProjection() {
+            return projection;
+        }
+
+        public Projection getSecondProjection() {
+            return projection2;
+        }
+
+        public Request getRequest() {
+            return request;
+        }
+
+        public boolean isComplete() {
+            return isComplete;
+        }
+
+        public boolean isSameLocation() {
+            return false;
+        }
+
+        public ProjectedRequest next() {
+            return next;
+        }
+
+        public boolean hasNext() {
+            return next != null;
+        }
+
+        protected final ProjectedRequest addNext( Request request,
+                                                  boolean isComplete,
+                                                  Projection projection,
+                                                  Projection secondProjection ) {
+            ProjectedRequest last = this;
+            while (last.next != null) {
+                last = last.next;
+            }
+            last.next = new ProjectedRequest(request, isComplete, projection, secondProjection);
+            return last.next;
+        }
+
+        protected final ProjectedRequest addNext( Request request,
+                                                  boolean isComplete,
+                                                  Projection projection ) {
+            return addNext(request, isComplete, projection, null);
+        }
+    }
+
+    class MirrorRequest extends ProjectedRequest {
+        protected MirrorRequest( Request request,
+                                 boolean isComplete,
+                                 Projection projection,
+                                 Projection secondProjection ) {
+            super(request, isComplete, projection, secondProjection);
+        }
+
+        @Override
+        public boolean isSameLocation() {
+            return true;
+        }
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedRequest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedWorkspace.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedWorkspace.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedWorkspace.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.collection.Problems;
+import org.jboss.dna.common.collection.ThreadSafeProblems;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryContext;
+
+/**
+ * The configuration of a single workspace within a {@link FederatedRepositorySource federated repository}.
+ */
+ at Immutable
+class FederatedWorkspace {
+
+    private final RepositoryContext repositoryContext;
+    private final String sourceName;
+    private final String workspaceName;
+    private final List<Projection> projections;
+    private final Map<String, List<Projection>> projectionsBySourceName;
+    private final CachePolicy cachePolicy;
+    private final Problems problems;
+    private final Projector projector;
+
+    /**
+     * Create a configuration for a federated workspace.
+     * 
+     * @param repositoryContext the repository context; may not be null
+     * @param sourceName the name of the federated repository; may not be null
+     * @param workspaceName the name of the federated workspace; may not be null
+     * @param projections the source projections; may not be null
+     * @param cachePolicy the cache policy for this workspace; may be null if there is no policy
+     * @throws IllegalArgumentException if the name is null or is blank
+     */
+    public FederatedWorkspace( RepositoryContext repositoryContext,
+                               String sourceName,
+                               String workspaceName,
+                               Iterable<Projection> projections,
+                               CachePolicy cachePolicy ) {
+        CheckArg.isNotNull(repositoryContext, "repositoryContext");
+        CheckArg.isNotNull(sourceName, "sourceName");
+        CheckArg.isNotNull(workspaceName, "workspaceName");
+        this.repositoryContext = repositoryContext;
+        this.workspaceName = workspaceName;
+        this.sourceName = sourceName;
+        this.cachePolicy = cachePolicy;
+        this.problems = new ThreadSafeProblems();
+        List<Projection> projectionList = new ArrayList<Projection>();
+        for (Projection projection : projections) {
+            if (projection == null) continue;
+            if (!projectionList.contains(projection)) {
+                projectionList.add(projection);
+            }
+        }
+        this.projections = Collections.unmodifiableList(projectionList);
+        CheckArg.isNotEmpty(this.projections, "sourceProjections");
+        this.projectionsBySourceName = new HashMap<String, List<Projection>>();
+        for (Projection projection : this.projections) {
+            String nameOfSource = projection.getSourceName();
+            List<Projection> projectionsForSource = projectionsBySourceName.get(nameOfSource);
+            if (projectionsForSource == null) {
+                projectionsForSource = new LinkedList<Projection>();
+                projectionsBySourceName.put(nameOfSource, projectionsForSource);
+            }
+            projectionsForSource.add(projection);
+        }
+
+        // Create the (most) appropriate projector ...
+        ExecutionContext context = this.repositoryContext.getExecutionContext();
+        Projector projector = MirrorProjector.with(context, projectionList);
+        if (projector == null) projector = BranchedMirrorProjector.with(context, projectionList);
+        if (projector == null) projector = OffsetMirrorProjector.with(context, projectionList);
+        if (projector == null) projector = GeneralProjector.with(context, projectionList);
+        assert projector != null;
+        this.projector = projector;
+    }
+
+    /**
+     * Get the repository context in which this workspace exists and has been initialized.
+     * 
+     * @return the repository context
+     */
+    public RepositoryContext getRepositoryContext() {
+        return repositoryContext;
+    }
+
+    /**
+     * Get the name of the federated repository.
+     * 
+     * @return sourceName
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * Get the name of this repository
+     * 
+     * @return name
+     */
+    public String getName() {
+        return this.workspaceName;
+    }
+
+    /**
+     * Get the cache policy for this workspace
+     * 
+     * @return the workspace's cache policy; may be null
+     */
+    public CachePolicy getCachePolicy() {
+        return cachePolicy;
+    }
+
+    /**
+     * Return the problem associated with this configuration. These problems may change at any time, although the returned
+     * {@link Problems} object is thread-safe.
+     * 
+     * @return the thread-safe problems for this configuration
+     */
+    public Problems getProblems() {
+        return problems;
+    }
+
+    /**
+     * Return the unmodifiable list of source projections.
+     * 
+     * @return the source projections; never null and never empty
+     */
+    public List<Projection> getProjections() {
+        return projections;
+    }
+
+    /**
+     * Determine whether this workspace has a projection supplied contribution
+     * 
+     * @param sourceName the name of the source
+     * @param workspaceName the name of the workspace
+     * @return true if this workspace contains a projection that uses the supplied source and workspace
+     */
+    public boolean contains( String sourceName,
+                             String workspaceName ) {
+        List<Projection> projections = this.projectionsBySourceName.get(sourceName);
+        if (projections != null) {
+            for (Projection projection : projections) {
+                if (projection.getWorkspaceName().equals(workspaceName)) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the map of projections by their source name. This method provides direct access to the map used by this instance, and
+     * is mutable. This is meant to be called only by subclasses and tests.
+     * 
+     * @return the list of projections for each source
+     */
+    Map<String, List<Projection>> getProjectionsBySourceName() {
+        return projectionsBySourceName;
+    }
+
+    /**
+     * Project the supplied location in the federated repository into the equivalent projected node(s).
+     * 
+     * @param context the execution context in which the content is being accessed; may not be null
+     * @param location the location in the federated repository; may not be null
+     * @param requiresUpdate true if the operation for which this projection is needed will update the content in some way, or
+     *        false if read-only operations will be performed
+     * @return the projected node, or null if the node does not exist in any projection
+     */
+    public ProjectedNode project( ExecutionContext context,
+                                  Location location,
+                                  boolean requiresUpdate ) {
+        return projector.project(context, location, requiresUpdate);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return this.workspaceName.hashCode();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof FederatedWorkspace) {
+            FederatedWorkspace that = (FederatedWorkspace)obj;
+            if (!this.getSourceName().equals(that.getSourceName())) return false;
+            if (!this.getName().equals(that.getName())) return false;
+            if (!this.getProjections().equals(that.getProjections())) return false;
+            return true;
+        }
+        return false;
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/FederatedWorkspace.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,1267 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.federation.FederatedRequest.ProjectedRequest;
+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.PathNotFoundException;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.request.CloneWorkspaceRequest;
+import org.jboss.dna.graph.request.CompositeRequest;
+import org.jboss.dna.graph.request.CopyBranchRequest;
+import org.jboss.dna.graph.request.CreateNodeRequest;
+import org.jboss.dna.graph.request.CreateWorkspaceRequest;
+import org.jboss.dna.graph.request.DeleteBranchRequest;
+import org.jboss.dna.graph.request.DeleteChildrenRequest;
+import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
+import org.jboss.dna.graph.request.GetWorkspacesRequest;
+import org.jboss.dna.graph.request.InvalidRequestException;
+import org.jboss.dna.graph.request.InvalidWorkspaceException;
+import org.jboss.dna.graph.request.MoveBranchRequest;
+import org.jboss.dna.graph.request.ReadAllChildrenRequest;
+import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
+import org.jboss.dna.graph.request.ReadBranchRequest;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.jboss.dna.graph.request.ReadPropertyRequest;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.UnsupportedRequestException;
+import org.jboss.dna.graph.request.UpdatePropertiesRequest;
+import org.jboss.dna.graph.request.VerifyNodeExistsRequest;
+import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * This is a {@link RequestProcessor} implementation that is responsible for forking each incoming request into the
+ * source-specific requests. This processor uses an {@link ExecutorService} to begin processing the forked requests immediately.
+ * As a result, while this processor processes the incoming requests on the federated content, the sources may already be
+ * processing previously forked (and source-specific) requests. Thus, it's quite possible that the sources finish processing their
+ * requests very shortly after this processor finishes its work.
+ * <p>
+ * This processor creates a separate channels for each source to which a request needs to be submitted. This channel submits all
+ * requests to that source via a single {@link RepositoryConnection#execute(ExecutionContext, Request) execute} call, meaning that
+ * all requests will be processed by the source within a single atomic operation. The channels also generally remain open until
+ * this processor completes processing of all incoming requests (that is, until all requests have been forked), so that any
+ * cancellation of this processor results in cancellation of each channel (and consequently the cancellation of the
+ * {@link CompositeRequest} that the channel submitted to its source).
+ * </p>
+ * 
+ * @see FederatedRepositoryConnection#execute(ExecutionContext, Request)
+ * @see JoinRequestProcessor
+ */
+ at NotThreadSafe
+class ForkRequestProcessor extends RequestProcessor {
+
+    /**
+     * A psuedo Request that is used by {@link Channel} to insert into a request queue so that the queue's iterator knows when
+     * there are no more requests to process.
+     */
+    protected static class LastRequest extends Request {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public boolean isReadOnly() {
+            return false;
+        }
+    }
+
+    /**
+     * A pseudo Request that allows addition of a request that, when processed, will decrement a latch. Since the latch is
+     * supplied by the submitter, this is useful when the submitter wishes to block until a particular request is processed. The
+     * submitter merely creates a {@link CountDownLatch}, submits their real request wrapped by a BlockedRequest, and calls
+     * {@link CountDownLatch#await()} on the latch. When <code>await()</code> returns, the first request has been completed.
+     * 
+     * @see ForkRequestProcessor#submit(Request, String, CountDownLatch)
+     * @see ForkRequestProcessor#submitAndAwait(Request, String)
+     */
+    protected static class BlockedRequest extends Request {
+        private static final long serialVersionUID = 1L;
+        protected final Request original;
+        protected final transient CountDownLatch latch;
+
+        protected BlockedRequest( Request original,
+                                  CountDownLatch latch ) {
+            this.original = original;
+            this.latch = latch;
+        }
+
+        @Override
+        public boolean isReadOnly() {
+            return original.isReadOnly();
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return original.isCancelled();
+        }
+    }
+
+    /**
+     * Represents the channel for a specific source into which this processor submits the requests for that source. To use, create
+     * a Channel, {@link Channel#start(ExecutorService, ExecutionContext, RepositoryConnectionFactory) start it}, and then
+     * {@link Channel#add(Request) add} requests (optionally with a {@link Channel#add(Request, CountDownLatch) latch} or via a
+     * {@link Channel#addAndAwait(Request) add and await}). Finally, call {@link Channel#done()} when there are no more requests.
+     * <p>
+     * When the channel is {@link Channel#start(ExecutorService, ExecutionContext, RepositoryConnectionFactory) started}, it
+     * creates a {@link Callable} and submits it to the supplied {@link ExecutorService}. (The resulting {@link Future} is then
+     * captured so that the channel can be {@link Channel#cancel(boolean) cancelled}.) The Callable obtains a
+     * {@link RepositoryConnection connection} to the channel's source, and then has the connection process a single
+     * {@link CompositeRequest} that fronts the queue of Request instances added to this channel. Because a blocking queue is
+     * used, the CompositeRequest's {@link CompositeRequest#iterator() iterator} blocks (on {@link Iterator#hasNext()}) until the
+     * next request is available. When {@link Channel#done()} is called, the iterator stops blocking and completes.
+     * </p>
+     */
+    protected static class Channel {
+        protected final String sourceName;
+        /** The list of all requests that are or have been processed as part of this channel */
+        protected final LinkedList<Request> allRequests = new LinkedList<Request>();
+        /** The queue of requests that remain unprocessed */
+        private final BlockingQueue<Request> queue = new LinkedBlockingQueue<Request>();
+        /** The CompositeRequest that is submitted to the underlying processor */
+        protected final CompositeRequest composite;
+        /** The Future that is submitted to the ExecutorService to do the processing */
+        protected Future<String> future;
+        /** Flag that defines whether the channel has processed all requests */
+        protected final AtomicBoolean done = new AtomicBoolean(false);
+
+        /**
+         * Create a new channel that operates against the supplied source.
+         * 
+         * @param sourceName the name of the repository source used to execute this channel's {@link #allRequests() requests}; may
+         *        not be null or empty
+         */
+        protected Channel( final String sourceName ) {
+            assert sourceName != null;
+            this.sourceName = sourceName;
+            this.composite = new CompositeRequest(false) {
+                private static final long serialVersionUID = 1L;
+                private final LinkedList<Request> allRequests = Channel.this.allRequests;
+
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.request.CompositeRequest#iterator()
+                 */
+                @Override
+                public Iterator<Request> iterator() {
+                    return createIterator();
+                }
+
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.request.CompositeRequest#getRequests()
+                 */
+                @Override
+                public List<Request> getRequests() {
+                    return allRequests;
+                }
+
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.request.CompositeRequest#size()
+                 */
+                @Override
+                public int size() {
+                    return done.get() ? allRequests.size() : CompositeRequest.UNKNOWN_NUMBER_OF_REQUESTS;
+                }
+
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see org.jboss.dna.graph.request.Request#cancel()
+                 */
+                @Override
+                public void cancel() {
+                    done.set(true);
+                }
+            };
+        }
+
+        /**
+         * Utility method to create an iterator over the requests in this channel. This really should be called once
+         * 
+         * @return the iterator over the channels
+         */
+        protected Iterator<Request> createIterator() {
+            final BlockingQueue<Request> queue = this.queue;
+            return new Iterator<Request>() {
+                private BlockedRequest previousBlocked;
+                private Request next;
+
+                public boolean hasNext() {
+                    // If the previous request was a blocked request, then we need to decrement its latch ...
+                    if (previousBlocked != null && previousBlocked.latch != null) {
+                        previousBlocked.latch.countDown();
+                    }
+
+                    // If next still has a request, then 'hasNext()' has been called multiple times in a row
+                    if (next != null) return true;
+
+                    // Now, block for a next item (this blocks) ...
+                    try {
+                        next = queue.take();
+                    } catch (InterruptedException e) {
+                        // This happens when the federated connector has been told to shutdown now, and it shuts down
+                        // its executor (the worker pool) immediately by interrupting each in-use thread.
+                        // In this case, we should consider there to be more more requests ...
+                        try {
+                            return false;
+                        } finally {
+                            // reset the interrupted status ...
+                            Thread.interrupted();
+                        }
+                    }
+                    if (next instanceof LastRequest) {
+                        return false;
+                    }
+                    return next != null;
+                }
+
+                public Request next() {
+                    if (next == null) {
+                        // Must have been called without first calling 'hasNext()' ...
+                        try {
+                            next = queue.take();
+                        } catch (InterruptedException e) {
+                            // This happens when the federated connector has been told to shutdown now, and it shuts down
+                            // its executor (the worker pool) immediately by interrupting each in-use thread.
+                            // In this case, we should consider there to be more more requests (again, this case
+                            // is when 'next()' has been called without calling 'hasNext()') ...
+                            try {
+                                throw new NoSuchElementException();
+                            } finally {
+                                // reset the interrupted status ...
+                                Thread.interrupted();
+                            }
+                        }
+                    }
+                    // If this request is a blocked request, we need to return the wrapped request and record the blocked request
+                    if (next instanceof BlockedRequest) {
+                        previousBlocked = (BlockedRequest)next;
+                        next = null;
+                        return previousBlocked.original;
+                    }
+                    previousBlocked = null;
+                    if (next != null) {
+                        Request result = next;
+                        next = null;
+                        return result;
+                    }
+                    throw new NoSuchElementException();
+                }
+
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        }
+
+        /**
+         * Begins processing any requests that have been {@link #add(Request) added} to this channel. Processing is done by
+         * submitting the channel to the supplied executor.
+         * 
+         * @param executor the executor that is to do the work; may not be null
+         * @param context the execution context in which the work is to be performed; may not be null
+         * @param connectionFactory the connection factory that should be used to create connections; may not be null
+         * @throws IllegalStateException if this channel has already been started
+         */
+        protected void start( final ExecutorService executor,
+                              final ExecutionContext context,
+                              final RepositoryConnectionFactory connectionFactory ) {
+            assert executor != null;
+            assert context != null;
+            assert connectionFactory != null;
+            if (this.future != null) {
+                throw new IllegalStateException();
+            }
+            this.future = executor.submit(new Callable<String>() {
+                /**
+                 * {@inheritDoc}
+                 * 
+                 * @see java.util.concurrent.Callable#call()
+                 */
+                public String call() throws Exception {
+                    final RepositoryConnection connection = connectionFactory.createConnection(sourceName);
+                    try {
+                        connection.execute(context, composite);
+                    } finally {
+                        connection.close();
+                    }
+                    return sourceName;
+                }
+            });
+        }
+
+        /**
+         * Add the request to this channel for asynchronous processing. This method is called by the
+         * {@link ForkRequestProcessor#submit(Request, String)} method.
+         * 
+         * @param request the request to be submitted; may not be null
+         * @throws IllegalStateException if this channel has already been marked as {@link #done()}
+         */
+        protected void add( Request request ) {
+            if (done.get()) {
+                throw new IllegalStateException(GraphI18n.unableToAddRequestToChannelThatIsDone.text(sourceName, request));
+            }
+            assert request != null;
+            this.allRequests.add(request);
+            this.queue.add(request);
+        }
+
+        /**
+         * Add the request to this channel for asynchronous processing, and supply a {@link CountDownLatch count-down latch} that
+         * should be {@link CountDownLatch#countDown() decremented} when this request is completed.
+         * 
+         * @param request the request to be submitted; may not be null
+         * @param latch the count-down latch that should be decremented when <code>request</code> has been completed; may not be
+         *        null
+         * @return the same latch that was supplied, for method chaining purposes; never null
+         * @throws IllegalStateException if this channel has already been marked as {@link #done()}
+         */
+        protected CountDownLatch add( Request request,
+                                      CountDownLatch latch ) {
+            if (done.get()) {
+                throw new IllegalStateException(GraphI18n.unableToAddRequestToChannelThatIsDone.text(sourceName, request));
+            }
+            assert request != null;
+            assert latch != null;
+            // Submit the request for processing ...
+            this.allRequests.add(request);
+            this.queue.add(new BlockedRequest(request, latch));
+            return latch;
+        }
+
+        /**
+         * Add the request to this channel for asynchronous processing, and supply a {@link CountDownLatch count-down latch} that
+         * should be {@link CountDownLatch#countDown() decremented} when this request is completed. This method is called by the
+         * {@link ForkRequestProcessor#submitAndAwait(Request, String)} method.
+         * 
+         * @param request the request to be submitted; may not be null
+         * @throws InterruptedException if the current thread is interrupted while waiting
+         */
+        protected void addAndAwait( Request request ) throws InterruptedException {
+            // Add the request with a latch, then block until the request has completed ...
+            add(request, new CountDownLatch(1)).await();
+        }
+
+        /**
+         * Mark this source as having no more requests to process.
+         */
+        protected void done() {
+            this.done.set(true);
+            this.queue.add(new LastRequest());
+        }
+
+        /**
+         * Return whether this channel has been {@link #done() marked as done}.
+         * 
+         * @return true if the channel was marked as done, or false otherwise
+         */
+        protected boolean isDone() {
+            return done.get();
+        }
+
+        /**
+         * Cancel this forked channel, stopping work as soon as possible. If the channel has not yet been started, this method
+         * 
+         * @param mayInterruptIfRunning true if the channel is still being worked on, and the thread on which its being worked on
+         *        may be interrupted, or false if the channel should be allowed to finish if it is already in work.
+         */
+        public void cancel( boolean mayInterruptIfRunning ) {
+            if (this.future == null || this.future.isDone() || this.future.isCancelled()) return;
+
+            // Mark the composite as cancelled first, so that the next composed request will be marked as
+            // cancelled.
+            this.composite.cancel();
+
+            // Now mark the channel as being done ...
+            done();
+
+            // Now, mark the channel as being cancelled (do allow interrupting the worker thread) ...
+            this.future.cancel(mayInterruptIfRunning);
+        }
+
+        /**
+         * Return whether this channel has been {@link #start(ExecutorService, ExecutionContext, RepositoryConnectionFactory)
+         * started}.
+         * 
+         * @return true if this channel was started, or false otherwise
+         */
+        public boolean isStarted() {
+            return this.future != null;
+        }
+
+        /**
+         * Return whether this channel has completed all of its work.
+         * 
+         * @return true if the channel was started and is complete, or false otherwise
+         */
+        public boolean isComplete() {
+            return this.future != null && this.future.isDone();
+        }
+
+        /**
+         * Await until this channel has completed.
+         * 
+         * @throws CancellationException if the channel was cancelled
+         * @throws ExecutionException if the channel execution threw an exception
+         * @throws InterruptedException if the current thread is interrupted while waiting
+         */
+        protected void await() throws ExecutionException, InterruptedException, CancellationException {
+            this.future.get();
+        }
+
+        /**
+         * Get all the requests that were submitted to this queue. The resulting list is the actual list that is appended when
+         * requests are added, and may change until the channel is marked as {@link #done() done}.
+         * 
+         * @return all of the requests that were submitted to this channel; never null
+         */
+        protected List<Request> allRequests() {
+            return allRequests;
+        }
+
+        /**
+         * Get the name of the source that this channel uses.
+         * 
+         * @return the source name; never null
+         */
+        protected String sourceName() {
+            return sourceName;
+        }
+    }
+
+    private final FederatedRepository repository;
+    private final ExecutorService executor;
+    private final RepositoryConnectionFactory connectionFactory;
+    private final Map<String, Channel> channelBySourceName = new HashMap<String, Channel>();
+    private final Queue<FederatedRequest> federatedRequestQueue;
+
+    /**
+     * Create a new fork processor
+     * 
+     * @param repository the federated repository configuration; never null
+     * @param context the execution context in which this processor is executing; may not be null
+     * @param now the timestamp representing the current time in UTC; may not be null
+     * @param federatedRequestQueue the queue into which should be placed the {@link FederatedRequest} objects created by this
+     *        processor that still must be post-processed
+     */
+    public ForkRequestProcessor( FederatedRepository repository,
+                                 ExecutionContext context,
+                                 DateTime now,
+                                 Queue<FederatedRequest> federatedRequestQueue ) {
+        super(repository.getSourceName(), context, now);
+        this.repository = repository;
+        this.executor = this.repository.getExecutor();
+        this.connectionFactory = this.repository.getConnectionFactory();
+        this.federatedRequestQueue = federatedRequestQueue;
+        assert this.executor != null;
+        assert this.connectionFactory != null;
+        assert this.federatedRequestQueue != null;
+    }
+
+    /**
+     * Asynchronously submit a request to the supplied source. This is typically called when the forked requests are not needed
+     * before continuing.
+     * 
+     * @param request the request to be submitted; may not be null
+     * @param sourceName the name of the source; may not be null
+     * @see #submitAndAwait(Request, String)
+     * @see #submit(Request, String, CountDownLatch)
+     */
+    protected void submit( Request request,
+                           String sourceName ) {
+        assert request != null;
+        Channel channel = channelBySourceName.get(sourceName);
+        if (channel == null) {
+            channel = new Channel(sourceName);
+            channelBySourceName.put(sourceName, channel);
+            channel.start(executor, getExecutionContext(), connectionFactory);
+        }
+        channel.add(request);
+    }
+
+    /**
+     * Submit a request to the supplied source, and block until the request has been processed. This method is typically used when
+     * a federated request is forked into multiple source-specific requests, but the output of a source-specific request is
+     * required before forking other source-specific requests. This pattern is common in requests that update one source and any
+     * information not stored by that source needs to be stored in another source.
+     * 
+     * @param request the request to be submitted; may not be null
+     * @param sourceName the name of the source; may not be null
+     * @throws InterruptedException if the current thread is interrupted while waiting
+     * @see #submit(Request, String)
+     * @see #submit(Request, String, CountDownLatch)
+     */
+    protected void submitAndAwait( Request request,
+                                   String sourceName ) throws InterruptedException {
+        assert request != null;
+        Channel channel = channelBySourceName.get(sourceName);
+        if (channel == null) {
+            channel = new Channel(sourceName);
+            channelBySourceName.put(sourceName, channel);
+            channel.start(executor, getExecutionContext(), connectionFactory);
+        }
+        channel.addAndAwait(request);
+    }
+
+    /**
+     * Submit a request to the supplied source, and have the supplied {@link CountDownLatch latch} be
+     * {@link CountDownLatch#countDown() decremented} when the request has been completed. Note that the same latch can be used
+     * multiple times.
+     * <p>
+     * This method is typically used when a federated request is forked into multiple source-specific requests, but the output of
+     * a source-specific request is required before forking other source-specific requests. This pattern is common in requests
+     * that update one source and any information not stored by that source needs to be stored in another source.
+     * </p>
+     * 
+     * @param request the request to be submitted; may not be null
+     * @param sourceName the name of the source; may not be null
+     * @param latch the count-down latch; may not be null
+     * @see #submit(Request, String)
+     * @see #submitAndAwait(Request, String)
+     */
+    protected void submit( Request request,
+                           String sourceName,
+                           CountDownLatch latch ) {
+        assert request != null;
+        Channel channel = channelBySourceName.get(sourceName);
+        if (channel == null) {
+            channel = new Channel(sourceName);
+            channelBySourceName.put(sourceName, channel);
+            channel.start(executor, getExecutionContext(), connectionFactory);
+        }
+        channel.add(request, latch);
+    }
+
+    /**
+     * Await until all of this processor's channels have completed.
+     * 
+     * @throws CancellationException if the channel was cancelled
+     * @throws ExecutionException if the channel execution threw an exception
+     * @throws InterruptedException if the current thread is interrupted while waiting
+     */
+    public void await() throws ExecutionException, InterruptedException, CancellationException {
+        for (Channel channel : channelBySourceName.values()) {
+            channel.await();
+        }
+    }
+
+    /**
+     * Utility to obtain the federated workspace referenced by the request. This method supports using the default workspace if
+     * the workspace name is null. If no such workspace, the request is marked with an appropriate error.
+     * 
+     * @param request the request; may not be null
+     * @param workspaceName the name of the workspace; may be null if the default workspace should be used
+     * @return the federated workspace, or null if none was found
+     */
+    protected final FederatedWorkspace getWorkspace( Request request,
+                                                     String workspaceName ) {
+        try {
+            return repository.getWorkspace(workspaceName);
+        } catch (InvalidWorkspaceException e) {
+            request.setError(e);
+        } catch (Throwable e) {
+            request.setError(e);
+        }
+        return null;
+    }
+
+    protected final String readable( Location location ) {
+        return location.getString(getExecutionContext().getNamespaceRegistry());
+    }
+
+    protected final String readable( Name name ) {
+        return name.getString(getExecutionContext().getNamespaceRegistry());
+    }
+
+    /**
+     * Utility method to project the specified location into the federated repository's sources.
+     * 
+     * @param location the location that should be projected; may not be null
+     * @param workspaceName the name of the workspace, or null if the default workspace should be used
+     * @param request the request; may not be null
+     * @param requiresUpdate true if the operation for which this projection is needed will update the content in some way, or
+     *        false if read-only operations will be performed
+     * @return the projected node, or null if there was no projected node (and an error was set on the request)
+     */
+    protected final ProjectedNode project( Location location,
+                                           String workspaceName,
+                                           Request request,
+                                           boolean requiresUpdate ) {
+        FederatedWorkspace workspace = getWorkspace(request, workspaceName);
+        if (workspace == null) return null;
+
+        ProjectedNode projectedNode = workspace.project(getExecutionContext(), location, requiresUpdate);
+        if (projectedNode == null) {
+            I18n msg = GraphI18n.locationCannotBeProjectedIntoWorkspaceAndSource;
+            Path root = getExecutionContext().getValueFactories().getPathFactory().createRootPath();
+            request.setError(new PathNotFoundException(location, root, msg.text(readable(location),
+                                                                                workspace.getName(),
+                                                                                repository.getSourceName())));
+        }
+        return projectedNode;
+    }
+
+    protected void submit( FederatedRequest request ) {
+        request.freeze();
+        if (request.hasIncompleteRequests()) {
+            // Submit the projected requests ...
+            ProjectedRequest projected = request.getFirstProjectedRequest();
+            while (projected != null) {
+                if (!projected.isComplete()) {
+                    submit(projected.getRequest(), projected.getProjection().getSourceName(), request.getLatch());
+                }
+                projected = projected.next();
+            }
+        }
+        // Record this federated request ...
+        this.federatedRequestQueue.add(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyNodeExistsRequest)
+     */
+    @Override
+    public void process( VerifyNodeExistsRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.at(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadNodeRequest placeholderRequest = new ReadNodeRequest(request.at(), request.inWorkspace());
+                placeholderRequest.addChildren(placeholder.children());
+                placeholderRequest.addProperties(placeholder.properties().values());
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                VerifyNodeExistsRequest pushDownRequest = new VerifyNodeExistsRequest(proxy.location(), proxy.workspaceName());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+        // Submit for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNodeRequest)
+     */
+    @Override
+    public void process( ReadNodeRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.at(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadNodeRequest placeholderRequest = new ReadNodeRequest(placeholder.location(), request.inWorkspace());
+                placeholderRequest.addChildren(placeholder.children());
+                placeholderRequest.addProperties(placeholder.properties().values());
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                ReadNodeRequest pushDownRequest = new ReadNodeRequest(proxy.location(), proxy.workspaceName());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+        // Submit for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
+     */
+    @Override
+    public void process( ReadAllChildrenRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.of(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadAllChildrenRequest placeholderRequest = new ReadAllChildrenRequest(placeholder.location(),
+                                                                                       request.inWorkspace());
+                placeholderRequest.addChildren(placeholder.children());
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                ReadAllChildrenRequest pushDownRequest = new ReadAllChildrenRequest(proxy.location(), proxy.workspaceName());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+        // Submit for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
+     */
+    @Override
+    public void process( ReadAllPropertiesRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.at(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadAllPropertiesRequest placeholderRequest = new ReadAllPropertiesRequest(placeholder.location(),
+                                                                                           request.inWorkspace());
+                placeholderRequest.addProperties(placeholder.properties().values());
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                ReadAllPropertiesRequest pushDownRequest = new ReadAllPropertiesRequest(proxy.location(), proxy.workspaceName());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+        // Submit for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadPropertyRequest)
+     */
+    @Override
+    public void process( ReadPropertyRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.on(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadPropertyRequest placeholderRequest = new ReadPropertyRequest(placeholder.location(), request.inWorkspace(),
+                                                                                 request.named());
+                Property property = placeholder.properties().get(request.named());
+                placeholderRequest.setProperty(property);
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                ReadPropertyRequest pushDownRequest = new ReadPropertyRequest(proxy.location(), proxy.workspaceName(),
+                                                                              request.named());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+        // Submit for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation creates a single {@link FederatedRequest} and a {@link ReadBranchRequest} for each {@link ProxyNode}
+     * and one {@link ReadNodeRequest} for each {@link PlaceholderNode}.
+     * </p>
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBranchRequest)
+     */
+    @Override
+    public void process( ReadBranchRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.at(), request.inWorkspace(), request, false);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        FederatedWorkspace workspace = getWorkspace(request, request.inWorkspace());
+
+        // And process the branch, creating ReadNodeRequests for each placeholder, and ReadBranchRequests for each proxy node...
+        if (request.maximumDepth() > 0) {
+            processBranch(federatedRequest, projectedNode, workspace, request.maximumDepth());
+        }
+
+        // Submit the requests for processing ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * A method used recursively to add {@link ReadNodeRequest}s and {@link ReadBranchRequest}s for each
+     * 
+     * @param federatedRequest
+     * @param projectedNode
+     * @param workspace
+     * @param maxDepth
+     */
+    protected void processBranch( FederatedRequest federatedRequest,
+                                  ProjectedNode projectedNode,
+                                  FederatedWorkspace workspace,
+                                  int maxDepth ) {
+        assert maxDepth > 0;
+        while (projectedNode != null) {
+            if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a request and set the results ...
+                ReadNodeRequest placeholderRequest = new ReadNodeRequest(placeholder.location(), workspace.getName());
+                placeholderRequest.addChildren(placeholder.children());
+                placeholderRequest.addProperties(placeholder.properties().values());
+                placeholderRequest.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(placeholderRequest, true, true, null);
+                if (maxDepth > 1) {
+                    ExecutionContext context = getExecutionContext();
+                    // For each child of the placeholder node ...
+                    for (Location child : placeholder.children()) {
+                        ProjectedNode projectedSubnode = workspace.project(context, child, false);
+                        // Call recursively, but reduce the max depth
+                        processBranch(federatedRequest, projectedSubnode, workspace, maxDepth - 1);
+                    }
+                }
+            } else if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                ReadBranchRequest pushDownRequest = new ReadBranchRequest(proxy.location(), proxy.workspaceName(), maxDepth);
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+            }
+            projectedNode = projectedNode.next();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
+     */
+    @Override
+    public void process( CreateNodeRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.under(), request.inWorkspace(), request, true);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+
+        // Any non-read request should be submitted to the first ProxyNode ...
+        while (projectedNode != null) {
+            if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                CreateNodeRequest pushDownRequest = new CreateNodeRequest(proxy.location(), proxy.workspaceName(),
+                                                                          request.named(), request.conflictBehavior(),
+                                                                          request.properties());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+
+                // Submit the requests for processing and then STOP ...
+                submit(federatedRequest);
+                return;
+            }
+            assert projectedNode.isPlaceholder();
+            projectedNode = projectedNode.next();
+        }
+        // Unable to perform this create ...
+        String msg = GraphI18n.unableToCreateNodeUnderPlaceholder.text(readable(request.named()),
+                                                                       readable(request.under()),
+                                                                       request.inWorkspace(),
+                                                                       getSourceName());
+        request.setError(new UnsupportedRequestException(msg));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
+     */
+    @Override
+    public void process( UpdatePropertiesRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.on(), request.inWorkspace(), request, true);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+
+        // Any non-read request should be submitted to the first ProxyNode ...
+        while (projectedNode != null) {
+            if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Create and submit a request for the projection ...
+                UpdatePropertiesRequest pushDownRequest = new UpdatePropertiesRequest(proxy.location(), proxy.workspaceName(),
+                                                                                      request.properties());
+                federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+
+                // Submit the requests for processing and then STOP ...
+                submit(federatedRequest);
+                return;
+            }
+            assert projectedNode.isPlaceholder();
+            projectedNode = projectedNode.next();
+        }
+        // Unable to perform this update ...
+        String msg = GraphI18n.unableToUpdatePlaceholder.text(readable(request.on()), request.inWorkspace(), getSourceName());
+        request.setError(new UnsupportedRequestException(msg));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
+     */
+    @Override
+    public void process( DeleteBranchRequest request ) {
+        // Figure out where this request is projected ...
+        ProjectedNode projectedNode = project(request.at(), request.inWorkspace(), request, true);
+        if (projectedNode == null) return;
+
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+
+        // A delete should be executed against any ProxyNode that applies ...
+        FederatedWorkspace workspace = getWorkspace(request, request.inWorkspace());
+        boolean submit = deleteBranch(federatedRequest, projectedNode, workspace, getExecutionContext());
+        if (submit) {
+            // Submit the requests for processing and then STOP ...
+            submit(federatedRequest);
+        } else {
+            // Unable to perform this delete ...
+            String msg = GraphI18n.unableToDeletePlaceholder.text(readable(request.at()), request.inWorkspace(), getSourceName());
+            request.setError(new UnsupportedRequestException(msg));
+        }
+    }
+
+    protected boolean deleteBranch( FederatedRequest federatedRequest,
+                                    ProjectedNode projectedNode,
+                                    FederatedWorkspace workspace,
+                                    ExecutionContext context ) {
+        boolean submit = false;
+        while (projectedNode != null) {
+            if (projectedNode.isProxy()) {
+                ProxyNode proxy = projectedNode.asProxy();
+                // Is this proxy represent the top level node of the projection?
+                if (proxy.isTopLevelNode()) {
+                    // Then we want to delete everything *underneath* the node, but we don't want to delete
+                    // the node itself since it is the node being projected and must exist in order for the
+                    // projection to work.
+                    DeleteChildrenRequest pushDownRequest = new DeleteChildrenRequest(proxy.location(), proxy.workspaceName());
+                    federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+                } else {
+                    // Create and submit a request for the projection ...
+                    DeleteBranchRequest pushDownRequest = new DeleteBranchRequest(proxy.location(), proxy.workspaceName());
+                    federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+                }
+                submit = true;
+            } else if (projectedNode.isPlaceholder()) {
+                PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                // Create a delete for this placeholder, but mark it completed. This is needed to know
+                // which placeholders were being deleted.
+                DeleteBranchRequest delete = new DeleteBranchRequest(placeholder.location(), workspace.getName());
+                delete.setActualLocationOfNode(placeholder.location());
+                federatedRequest.add(delete, true, true, null);
+                // Create and submit a request for each proxy below this placeholder ...
+                // For each child of the placeholder node ...
+                for (Location child : placeholder.children()) {
+                    ProjectedNode projectedSubnode = workspace.project(context, child, true);
+                    // Call recursively ...
+                    submit = deleteBranch(federatedRequest, projectedSubnode, workspace, context);
+                }
+            }
+            projectedNode = projectedNode.next();
+        }
+        return submit;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
+     */
+    @Override
+    public void process( CopyBranchRequest request ) {
+        // Figure out where the 'from' is projected ...
+        ProjectedNode projectedFromNode = project(request.from(), request.fromWorkspace(), request, false);
+        if (projectedFromNode == null) return;
+        ProjectedNode projectedIntoNode = project(request.into(), request.intoWorkspace(), request, true);
+        if (projectedIntoNode == null) return;
+
+        // Limitation: only able to project the copy if the 'from' and 'into' are in the same source & projection ...
+        while (projectedFromNode != null) {
+            if (projectedFromNode.isProxy()) {
+                ProxyNode fromProxy = projectedFromNode.asProxy();
+                // Look for a projectedIntoNode that has the same source/projection ...
+                while (projectedIntoNode != null) {
+                    if (projectedIntoNode.isProxy()) {
+                        // Both are proxies, so compare the projection ...
+                        ProxyNode intoProxy = projectedIntoNode.asProxy();
+                        if (fromProxy.projection().getSourceName().equals(intoProxy.projection().getSourceName())) break;
+                    }
+                    projectedIntoNode = projectedIntoNode.next();
+                }
+                if (projectedIntoNode != null) break;
+            }
+            projectedFromNode = projectedFromNode.next();
+        }
+        if (projectedFromNode == null || projectedIntoNode == null) {
+            // The copy is not done within a single source ...
+            String msg = GraphI18n.copyLimitedToBeWithinSingleSource.text(readable(request.from()),
+                                                                          request.fromWorkspace(),
+                                                                          readable(request.into()),
+                                                                          request.intoWorkspace(),
+                                                                          getSourceName());
+            request.setError(new UnsupportedRequestException(msg));
+            return;
+        }
+
+        ProxyNode fromProxy = projectedFromNode.asProxy();
+        ProxyNode intoProxy = projectedIntoNode.asProxy();
+        assert fromProxy.projection().getSourceName().equals(intoProxy.projection().getSourceName());
+        boolean sameLocation = fromProxy.isSameLocationAsOriginal() && intoProxy.isSameLocationAsOriginal();
+
+        // Create the pushed-down request ...
+        CopyBranchRequest pushDown = new CopyBranchRequest(fromProxy.location(), fromProxy.workspaceName(), intoProxy.location(),
+                                                           intoProxy.workspaceName(), request.desiredName(),
+                                                           request.conflictBehavior());
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        federatedRequest.add(pushDown, sameLocation, false, fromProxy.projection(), intoProxy.projection());
+
+        // Submit the requests for processing and then STOP ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
+     */
+    @Override
+    public void process( MoveBranchRequest request ) {
+        // Figure out where the 'from' is projected ...
+        ProjectedNode projectedFromNode = project(request.from(), request.inWorkspace(), request, true);
+        if (projectedFromNode == null) return;
+        ProjectedNode projectedIntoNode = project(request.into(), request.inWorkspace(), request, true);
+        if (projectedIntoNode == null) return;
+
+        // Limitation: only able to project the move if the 'from' and 'into' are in the same source & projection ...
+        while (projectedFromNode != null) {
+            if (projectedFromNode.isProxy()) {
+                ProxyNode fromProxy = projectedFromNode.asProxy();
+                // Look for a projectedIntoNode that has the same source/projection ...
+                while (projectedIntoNode != null) {
+                    if (projectedIntoNode.isProxy()) {
+                        // Both are proxies, so compare the projection ...
+                        ProxyNode intoProxy = projectedIntoNode.asProxy();
+                        if (fromProxy.projection().getSourceName().equals(intoProxy.projection().getSourceName())) break;
+                    }
+                    projectedIntoNode = projectedIntoNode.next();
+                }
+                if (projectedIntoNode != null) break;
+            }
+            projectedFromNode = projectedFromNode.next();
+        }
+        if (projectedFromNode == null || projectedIntoNode == null) {
+            // The move is not done within a single source ...
+            String msg = GraphI18n.moveLimitedToBeWithinSingleSource.text(readable(request.from()),
+                                                                          request.inWorkspace(),
+                                                                          readable(request.into()),
+                                                                          request.inWorkspace(),
+                                                                          getSourceName());
+            request.setError(new UnsupportedRequestException(msg));
+            return;
+        }
+
+        ProxyNode fromProxy = projectedFromNode.asProxy();
+        ProxyNode intoProxy = projectedIntoNode.asProxy();
+        assert fromProxy.projection().getSourceName().equals(intoProxy.projection().getSourceName());
+        boolean sameLocation = fromProxy.isSameLocationAsOriginal() && intoProxy.isSameLocationAsOriginal();
+
+        // Create the pushed-down request ...
+        MoveBranchRequest pushDown = new MoveBranchRequest(fromProxy.location(), intoProxy.location(), intoProxy.workspaceName(),
+                                                           request.desiredName(), request.conflictBehavior());
+        // Create the federated request ...
+        FederatedRequest federatedRequest = new FederatedRequest(request);
+        federatedRequest.add(pushDown, sameLocation, false, fromProxy.projection(), intoProxy.projection());
+
+        // Submit the requests for processing and then STOP ...
+        submit(federatedRequest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
+     */
+    @Override
+    public void process( VerifyWorkspaceRequest request ) {
+        FederatedWorkspace workspace = getWorkspace(request, request.workspaceName());
+        if (workspace != null) {
+            request.setActualWorkspaceName(workspace.getName());
+
+            // Get the root location ...
+            Location root = Location.create(getExecutionContext().getValueFactories().getPathFactory().createRootPath());
+            ProjectedNode projectedNode = project(root, workspace.getName(), request, false);
+            if (projectedNode == null) return;
+
+            // Create the federated request ...
+            FederatedRequest federatedRequest = new FederatedRequest(request);
+            while (projectedNode != null) {
+                if (projectedNode.isPlaceholder()) {
+                    PlaceholderNode placeholder = projectedNode.asPlaceholder();
+                    // Create a request and set the results ...
+                    VerifyNodeExistsRequest placeholderRequest = new VerifyNodeExistsRequest(root, workspace.getName());
+                    placeholderRequest.setActualLocationOfNode(placeholder.location());
+                    federatedRequest.add(placeholderRequest, true, true, null);
+                } else if (projectedNode.isProxy()) {
+                    ProxyNode proxy = projectedNode.asProxy();
+                    // Create and submit a request for the projection ...
+                    VerifyNodeExistsRequest pushDownRequest = new VerifyNodeExistsRequest(proxy.location(), proxy.workspaceName());
+                    federatedRequest.add(pushDownRequest, proxy.isSameLocationAsOriginal(), false, proxy.projection());
+                }
+                projectedNode = projectedNode.next();
+            }
+            // Submit for processing ...
+            submit(federatedRequest);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
+     */
+    @Override
+    public void process( GetWorkspacesRequest request ) {
+        request.setAvailableWorkspaceNames(repository.getWorkspaceNames());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
+     */
+    @Override
+    public void process( CreateWorkspaceRequest request ) {
+        String msg = GraphI18n.federatedSourceDoesNotSupportCreatingWorkspaces.text(getSourceName());
+        request.setError(new InvalidRequestException(msg));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
+     */
+    @Override
+    public void process( CloneWorkspaceRequest request ) {
+        String msg = GraphI18n.federatedSourceDoesNotSupportCloningWorkspaces.text(getSourceName());
+        request.setError(new InvalidRequestException(msg));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
+     */
+    @Override
+    public void process( DestroyWorkspaceRequest request ) {
+        String msg = GraphI18n.federatedSourceDoesNotSupportDestroyingWorkspaces.text(getSourceName());
+        request.setError(new InvalidRequestException(msg));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#close()
+     */
+    @Override
+    public void close() {
+        super.close();
+        for (Channel channel : channelBySourceName.values()) {
+            channel.done();
+        }
+    }
+
+    protected void cancel( boolean mayInterruptIfRunning ) {
+        for (Channel channel : channelBySourceName.values()) {
+            channel.cancel(mayInterruptIfRunning);
+        }
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/GeneralProjector.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/GeneralProjector.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/GeneralProjector.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,121 @@
+/*
+ * 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.connector.federation;
+
+import java.util.List;
+import java.util.Set;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+
+/**
+ * A Projector for federated repository configurations that are an offset, direct one-for-one mirror against a single source
+ * repository that is projected below the federated root. In other words, the federated repository has a single projection with a
+ * single "/something/below/root => /" rule.
+ */
+ at Immutable
+final class GeneralProjector extends ProjectorWithPlaceholders {
+
+    /**
+     * Attempt to create an instance of the {@link GeneralProjector} with the supplied projections using the supplied context.
+     * 
+     * @param context the context; may not be null
+     * @param projections the projections in the federated repository; may not be null
+     * @return the offset mirror projector, or null if the projections didn't match the criteria for such a projector
+     */
+    static GeneralProjector with( ExecutionContext context,
+                                  List<Projection> projections ) {
+        assert projections != null;
+        assert context != null;
+        return new GeneralProjector(context, projections);
+    }
+
+    private final List<Projection> projections;
+
+    private GeneralProjector( ExecutionContext context,
+                              List<Projection> projections ) {
+        super(context, projections);
+        this.projections = projections;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.federation.Projector#project(ExecutionContext, Location, boolean)
+     */
+    public ProjectedNode project( ExecutionContext context,
+                                  Location location,
+                                  boolean requiresUpdate ) {
+        // Find the location of the desired node ...
+        Path path = location.getPath();
+        if (path == null) {
+            // There are only identification properties, so return a ProxyNode for each source/workspace ...
+            ProjectedNode result = null;
+            for (Projection projection : projections) {
+                if (requiresUpdate && projection.isReadOnly()) continue;
+                ProxyNode proxy = new ProxyNode(projection, location, location, false);
+                if (result == null) {
+                    result = proxy;
+                } else {
+                    result.add(proxy);
+                }
+            }
+            return result;
+        }
+        // We have a path, so see if the path is one of the existing placeholder nodes ...
+        ProjectedNode result = isPlaceholder(location);
+        if (result != null) return result;
+
+        // Otherwise this node lives below one (or more) of the projections, and we figure this out each time ...
+        final PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        for (Projection projection : projections) {
+            if (requiresUpdate && projection.isReadOnly()) continue;
+            // Project the federated repository path into the paths as they would exist in the source ...
+            Set<Path> pathsInSource = projection.getPathsInSource(path, pathFactory);
+            for (Path pathInSource : pathsInSource) {
+                // Create a ProxyNode for this projection ...
+                Location locationInSource = Location.create(pathInSource);
+                boolean samePath = pathInSource.equals(path);
+                ProxyNode proxy = new ProxyNode(projection, locationInSource, location, samePath);
+                if (result == null) {
+                    result = proxy;
+                } else {
+                    result.add(proxy);
+                }
+            }
+        }
+
+        // If there is only one projection ...
+        if (result != null && !result.hasNext() && result.isProxy()) {
+            // Then we want to include the supplied location's ID properties down to the source ...
+            ProxyNode proxy = result.asProxy();
+            result = new ProxyNode(proxy.projection(), location.with(result.location().getPath()), location,
+                                   proxy.isSameLocationAsOriginal());
+        }
+        return result;
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/GeneralProjector.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinMirrorRequestProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinMirrorRequestProcessor.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinMirrorRequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,391 @@
+/*
+ * 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.connector.federation;
+
+import java.util.List;
+import java.util.Map;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.request.CacheableRequest;
+import org.jboss.dna.graph.request.CloneWorkspaceRequest;
+import org.jboss.dna.graph.request.CopyBranchRequest;
+import org.jboss.dna.graph.request.CreateNodeRequest;
+import org.jboss.dna.graph.request.CreateWorkspaceRequest;
+import org.jboss.dna.graph.request.DeleteBranchRequest;
+import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
+import org.jboss.dna.graph.request.GetWorkspacesRequest;
+import org.jboss.dna.graph.request.MoveBranchRequest;
+import org.jboss.dna.graph.request.ReadAllChildrenRequest;
+import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
+import org.jboss.dna.graph.request.ReadBlockOfChildrenRequest;
+import org.jboss.dna.graph.request.ReadBranchRequest;
+import org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.jboss.dna.graph.request.ReadPropertyRequest;
+import org.jboss.dna.graph.request.RemovePropertyRequest;
+import org.jboss.dna.graph.request.RenameNodeRequest;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.SetPropertyRequest;
+import org.jboss.dna.graph.request.UpdatePropertiesRequest;
+import org.jboss.dna.graph.request.VerifyNodeExistsRequest;
+import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * A RequestProcessor used by the {@link JoinRequestProcessor} in cases where there is a single source-request with the same
+ * location(s). This processor merely copies the results from each source request for the
+ * {@link #setFederatedRequest(FederatedRequest) current federated request} into the request passed as a parameter.
+ */
+class JoinMirrorRequestProcessor extends RequestProcessor {
+
+    private FederatedRequest federatedRequest;
+
+    JoinMirrorRequestProcessor( String sourceName,
+                                ExecutionContext context,
+                                DateTime now,
+                                CachePolicy defaultCachePolicy ) {
+        super(sourceName, context, now, defaultCachePolicy);
+
+    }
+
+    /**
+     * @param federatedRequest Sets federatedRequest to the specified value.
+     */
+    void setFederatedRequest( FederatedRequest federatedRequest ) {
+        this.federatedRequest = federatedRequest;
+    }
+
+    protected boolean checkErrorOrCancel( Request request,
+                                          Request sourceRequest ) {
+        if (sourceRequest.hasError()) {
+            request.setError(sourceRequest.getError());
+            return true;
+        }
+        if (sourceRequest.isCancelled()) {
+            request.cancel();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNodeRequest)
+     */
+    @Override
+    public void process( ReadNodeRequest request ) {
+        ReadNodeRequest source = (ReadNodeRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        for (Location childInSource : source.getChildren()) {
+            request.addChild(childInSource);
+        }
+        for (Property propertyInSource : source.getProperties()) {
+            request.addProperties(propertyInSource);
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
+     */
+    @Override
+    public void process( ReadAllChildrenRequest request ) {
+        ReadAllChildrenRequest source = (ReadAllChildrenRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        for (Location childInSource : source.getChildren()) {
+            request.addChild(childInSource);
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
+     */
+    @Override
+    public void process( ReadAllPropertiesRequest request ) {
+        ReadAllPropertiesRequest source = (ReadAllPropertiesRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        for (Property propertyInSource : source.getProperties()) {
+            request.addProperties(propertyInSource);
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBlockOfChildrenRequest)
+     */
+    @Override
+    public void process( ReadBlockOfChildrenRequest request ) {
+        ReadBlockOfChildrenRequest source = (ReadBlockOfChildrenRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        for (Location childInSource : source.getChildren()) {
+            request.addChild(childInSource);
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest)
+     */
+    @Override
+    public void process( ReadNextBlockOfChildrenRequest request ) {
+        ReadNextBlockOfChildrenRequest source = (ReadNextBlockOfChildrenRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfStartingAfterNode(source.getActualLocationOfStartingAfterNode());
+        for (Location childInSource : source.getChildren()) {
+            request.addChild(childInSource);
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadPropertyRequest)
+     */
+    @Override
+    public void process( ReadPropertyRequest request ) {
+        ReadPropertyRequest source = (ReadPropertyRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        request.setProperty(source.getProperty());
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.SetPropertyRequest)
+     */
+    @Override
+    public void process( SetPropertyRequest request ) {
+        SetPropertyRequest source = (SetPropertyRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyNodeExistsRequest)
+     */
+    @Override
+    public void process( VerifyNodeExistsRequest request ) {
+        VerifyNodeExistsRequest source = (VerifyNodeExistsRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.RemovePropertyRequest)
+     */
+    @Override
+    public void process( RemovePropertyRequest request ) {
+        RemovePropertyRequest source = (RemovePropertyRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.RenameNodeRequest)
+     */
+    @Override
+    public void process( RenameNodeRequest request ) {
+        RenameNodeRequest source = (RenameNodeRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocations(source.getActualLocationBefore(), source.getActualLocationAfter());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBranchRequest)
+     */
+    @Override
+    public void process( ReadBranchRequest request ) {
+        CacheableRequest source = (CacheableRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        if (source instanceof ReadBranchRequest) {
+            ReadBranchRequest readSource = (ReadBranchRequest)source;
+            request.setActualLocationOfNode(readSource.getActualLocationOfNode());
+            for (Location node : readSource) {
+                List<Location> children = readSource.getChildren(node);
+                if (children != null) request.setChildren(node, children);
+                Map<Name, Property> props = readSource.getPropertiesFor(node);
+                if (props != null) request.setProperties(node, props.values());
+            }
+        } else if (source instanceof ReadNodeRequest) {
+            ReadNodeRequest readSource = (ReadNodeRequest)source;
+            request.setActualLocationOfNode(readSource.getActualLocationOfNode());
+            Location parent = readSource.getActualLocationOfNode();
+            request.setChildren(parent, readSource.getChildren());
+            request.setProperties(parent, readSource.getPropertiesByName().values());
+        }
+        request.setCachePolicy(getDefaultCachePolicy());
+        setCacheableInfo(request, source.getCachePolicy());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
+     */
+    @Override
+    public void process( CreateNodeRequest request ) {
+        CreateNodeRequest source = (CreateNodeRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
+     */
+    @Override
+    public void process( UpdatePropertiesRequest request ) {
+        UpdatePropertiesRequest source = (UpdatePropertiesRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
+     */
+    @Override
+    public void process( CopyBranchRequest request ) {
+        CopyBranchRequest source = (CopyBranchRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocations(source.getActualLocationBefore(), source.getActualLocationAfter());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
+     */
+    @Override
+    public void process( DeleteBranchRequest request ) {
+        DeleteBranchRequest source = (DeleteBranchRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocationOfNode(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
+     */
+    @Override
+    public void process( MoveBranchRequest request ) {
+        MoveBranchRequest source = (MoveBranchRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        request.setActualLocations(source.getActualLocationBefore(), source.getActualLocationAfter());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
+     */
+    @Override
+    public void process( VerifyWorkspaceRequest request ) {
+        VerifyNodeExistsRequest source = (VerifyNodeExistsRequest)federatedRequest.getFirstProjectedRequest().getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        assert source.getActualLocationOfNode().getPath().isRoot();
+        request.setActualRootLocation(source.getActualLocationOfNode());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
+     */
+    @Override
+    public void process( GetWorkspacesRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
+     */
+    @Override
+    public void process( CreateWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
+     */
+    @Override
+    public void process( CloneWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
+     */
+    @Override
+    public void process( DestroyWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinMirrorRequestProcessor.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessor.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,981 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.concurrent.BlockingQueue;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.federation.FederatedRequest.ProjectedRequest;
+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.PathFactory;
+import org.jboss.dna.graph.property.PathNotFoundException;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.property.PropertyFactory;
+import org.jboss.dna.graph.property.ValueComparators;
+import org.jboss.dna.graph.request.CacheableRequest;
+import org.jboss.dna.graph.request.CloneWorkspaceRequest;
+import org.jboss.dna.graph.request.CopyBranchRequest;
+import org.jboss.dna.graph.request.CreateNodeRequest;
+import org.jboss.dna.graph.request.CreateWorkspaceRequest;
+import org.jboss.dna.graph.request.DeleteBranchRequest;
+import org.jboss.dna.graph.request.DeleteChildrenRequest;
+import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
+import org.jboss.dna.graph.request.GetWorkspacesRequest;
+import org.jboss.dna.graph.request.InvalidRequestException;
+import org.jboss.dna.graph.request.MoveBranchRequest;
+import org.jboss.dna.graph.request.ReadAllChildrenRequest;
+import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
+import org.jboss.dna.graph.request.ReadBranchRequest;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.jboss.dna.graph.request.ReadPropertyRequest;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.UpdatePropertiesRequest;
+import org.jboss.dna.graph.request.VerifyNodeExistsRequest;
+import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * 
+ */
+ at NotThreadSafe
+class JoinRequestProcessor extends RequestProcessor {
+
+    private final PathFactory pathFactory;
+    private final PropertyFactory propertyFactory;
+    private final JoinMirrorRequestProcessor mirrorProcessor;
+    protected FederatedRequest federatedRequest;
+
+    /**
+     * Create a new join processor
+     * 
+     * @param repository the federated repository configuration; never null
+     * @param context the execution context in which this processor is executing; may not be null
+     * @param now the timestamp representing the current time in UTC; may not be null
+     */
+    public JoinRequestProcessor( FederatedRepository repository,
+                                 ExecutionContext context,
+                                 DateTime now ) {
+        super(repository.getSourceName(), context, now, repository.getDefaultCachePolicy());
+        // this.repository = repository;
+        this.propertyFactory = context.getPropertyFactory();
+        this.pathFactory = context.getValueFactories().getPathFactory();
+        this.mirrorProcessor = new JoinMirrorRequestProcessor(repository.getSourceName(), context, now,
+                                                              repository.getDefaultCachePolicy());
+    }
+
+    /**
+     * Process all of the {@link FederatedRequest} objects that are in the supplied collection.
+     * 
+     * @param completedFederatedRequests the collection of {@link FederatedRequest} whose projected requests have already been
+     *        processed; may not be null
+     * @see FederatedRepositoryConnection#execute(ExecutionContext, org.jboss.dna.graph.request.Request)
+     */
+    public void process( final Iterable<FederatedRequest> completedFederatedRequests ) {
+        for (FederatedRequest federatedRequest : completedFederatedRequests) {
+            // No need to await for the forked request, since it will be done
+            this.federatedRequest = federatedRequest;
+            process(federatedRequest.original());
+        }
+    }
+
+    /**
+     * Process the {@link FederatedRequest} objects that are in the supplied queue. The queue contains {@link FederatedRequest}
+     * that may have projected requests that have not yet been processed by the respective source, so this method
+     * {@link FederatedRequest#await() waits} until all source requests have been processed. This method returns only when it
+     * obtains from the queue a {@link NoMoreFederatedRequests} instance.
+     * 
+     * @param federatedRequestQueue the queue containing the federated requests; may not be null
+     * @see FederatedRepositoryConnection#execute(ExecutionContext, org.jboss.dna.graph.request.Request)
+     */
+    public void process( final BlockingQueue<FederatedRequest> federatedRequestQueue ) {
+        FederatedRequest forked = null;
+        try {
+            for (;;) {
+                forked = federatedRequestQueue.take();
+                if (forked instanceof NoMoreFederatedRequests) return;
+                forked.await();
+
+                // Determine whether this is a single mirror request ...
+                Request original = forked.original();
+                ProjectedRequest projectedRequest = forked.getFirstProjectedRequest();
+                boolean sameLocation = projectedRequest != null && !projectedRequest.hasNext()
+                                       && projectedRequest.isSameLocation();
+
+                // Set the cachable information ...
+                if (original instanceof CacheableRequest) {
+                    CacheableRequest cacheableOriginal = (CacheableRequest)original;
+                    cacheableOriginal.setCachePolicy(getDefaultCachePolicy());
+                    while (projectedRequest != null) {
+                        Request requestToSource = projectedRequest.getRequest();
+                        if (cacheableOriginal != null) {
+                            setCacheableInfo(cacheableOriginal, ((CacheableRequest)requestToSource).getCachePolicy());
+                        }
+                        projectedRequest = projectedRequest.next();
+                    }
+                }
+
+                // Now do the join on this request ...
+                if (sameLocation) {
+                    Request sourceRequest = forked.getFirstProjectedRequest().getRequest();
+                    if (sourceRequest.hasError()) {
+                        original.setError(sourceRequest.getError());
+                    } else if (sourceRequest.isCancelled()) {
+                        original.cancel();
+                    }
+                    mirrorProcessor.setFederatedRequest(forked);
+                    mirrorProcessor.process(original);
+                } else {
+                    this.federatedRequest = forked;
+                    process(original);
+                }
+            }
+        } catch (InterruptedException e) {
+            // This happens when the federated connector has been told to shutdown now, and it shuts down
+            // its executor (the worker pool) immediately by interrupting each in-use thread.
+            // In this case, we should cancel the current request but should NOT iterate over any remaining requests.
+            try {
+                if (forked != null) {
+                    forked.original().cancel();
+                }
+            } finally {
+                // Clear the interrupted status of the thread ...
+                Thread.interrupted();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyNodeExistsRequest)
+     */
+    @Override
+    public void process( VerifyNodeExistsRequest request ) {
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.at();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            VerifyNodeExistsRequest readFromSource = (VerifyNodeExistsRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+            setCacheableInfo(request, readFromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.at(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNodeRequest)
+     */
+    @Override
+    public void process( ReadNodeRequest request ) {
+        Path federatedPath = request.at().getPath();
+        Map<Name, Property> properties = request.getPropertiesByName();
+        Map<Name, Integer> childSnsIndexes = new HashMap<Name, Integer>();
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.at();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            ReadNodeRequest readFromSource = (ReadNodeRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+
+            // Add all the children from the source ...
+            for (Location childInSource : readFromSource.getChildren()) {
+                // Project back into the federated repository ...
+                Path childPath = childInSource.getPath();
+                // Correct the same-name-sibling index for the child ...
+                Name childName = childPath.getLastSegment().getName();
+                Integer snsIndex = childSnsIndexes.get(childName);
+                if (snsIndex == null) {
+                    snsIndex = new Integer(1);
+                    childSnsIndexes.put(childName, snsIndex);
+                } else {
+                    snsIndex = new Integer(snsIndex.intValue() + 1);
+                    childSnsIndexes.put(childName, snsIndex);
+                }
+                Path newPath = pathFactory.create(federatedPath, childName, snsIndex.intValue());
+                request.addChild(childInSource.with(newPath));
+            }
+
+            // Add all the properties ...
+            for (Property propertyInSource : readFromSource.getProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = properties.get(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                properties.put(name, propertyInSource);
+            }
+            setCacheableInfo(request, readFromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.at(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * Sets the request {@link Request#setError(Throwable) error} to a {@link PathNotFoundException} that has the lowest existing
+     * ancestor computed from the {@link PathNotFoundException}s in the projected requests.
+     * 
+     * @param original
+     * @param originalLocation
+     * @param projected
+     */
+    protected void setPathNotFound( Request original,
+                                    Location originalLocation,
+                                    ProjectedRequest projected ) {
+        Path lowestExistingInFederated = pathFactory.createRootPath();
+        while (projected != null) {
+            Request projectedRequest = projected.getRequest();
+            Throwable error = projectedRequest.getError();
+            if (error instanceof PathNotFoundException) {
+                PathNotFoundException notFound = (PathNotFoundException)error;
+                Path lowestExisting = notFound.getLowestAncestorThatDoesExist();
+                // Project back to the repository level ...
+                for (Path federatedPath : projected.getProjection().getPathsInRepository(lowestExisting, pathFactory)) {
+                    if (federatedPath.isAtOrBelow(lowestExistingInFederated)) {
+                        lowestExistingInFederated = federatedPath;
+                    }
+                }
+            }
+            projected = projected.next();
+        }
+        original.setError(new PathNotFoundException(originalLocation, lowestExistingInFederated));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
+     */
+    @Override
+    public void process( ReadAllChildrenRequest request ) {
+        Path federatedPath = request.of().getPath();
+        Map<Name, Integer> childSnsIndexes = new HashMap<Name, Integer>();
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.of();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            ReadAllChildrenRequest readFromSource = (ReadAllChildrenRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+
+            // Add all the children from the source ...
+            for (Location childInSource : readFromSource.getChildren()) {
+                // Project back into the federated repository ...
+                Path childPath = childInSource.getPath();
+                // Correct the same-name-sibling index for the child ...
+                Name childName = childPath.getLastSegment().getName();
+                Integer snsIndex = childSnsIndexes.get(childName);
+                if (snsIndex == null) {
+                    snsIndex = new Integer(1);
+                    childSnsIndexes.put(childName, snsIndex);
+                } else {
+                    snsIndex = new Integer(snsIndex.intValue() + 1);
+                    childSnsIndexes.put(childName, snsIndex);
+                }
+                Path newPath = pathFactory.create(federatedPath, childName, snsIndex.intValue());
+                request.addChild(childInSource.with(newPath));
+            }
+
+            setCacheableInfo(request, readFromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.of(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
+     */
+    @Override
+    public void process( ReadAllPropertiesRequest request ) {
+        Map<Name, Property> properties = request.getPropertiesByName();
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.at();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            ReadAllPropertiesRequest readFromSource = (ReadAllPropertiesRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+
+            // Add all the properties ...
+            for (Property propertyInSource : readFromSource.getProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = properties.get(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                properties.put(name, propertyInSource);
+            }
+            setCacheableInfo(request, readFromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.at(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadPropertyRequest)
+     */
+    @Override
+    public void process( ReadPropertyRequest request ) {
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.on();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            ReadPropertyRequest readFromSource = (ReadPropertyRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+
+            // Add all the properties ...
+            Property read = readFromSource.getProperty();
+            if (read != null) {
+                Property existing = request.getProperty();
+                if (existing != null) {
+                    // Merge the property values ...
+                    request.setProperty(merge(existing, read, propertyFactory, true));
+                } else {
+                    request.setProperty(read);
+                }
+            }
+            setCacheableInfo(request, readFromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.on(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBranchRequest)
+     */
+    @Override
+    public void process( ReadBranchRequest request ) {
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        request.setCachePolicy(getDefaultCachePolicy());
+        Location actualLocation = request.at();
+        int numMerged = 0;
+        while (projectedRequest != null) {
+            CacheableRequest fromSource = (CacheableRequest)projectedRequest.getRequest();
+            if (fromSource.hasError()) {
+                projectedRequest = projectedRequest.next();
+                continue;
+            }
+            if (fromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            Projection projection = projectedRequest.getProjection();
+            if (fromSource instanceof ReadNodeRequest) {
+                ReadNodeRequest readFromSource = (ReadNodeRequest)fromSource;
+                Location parent = readFromSource.getActualLocationOfNode();
+                List<Location> children = readFromSource.getChildren();
+                Map<Name, Property> properties = readFromSource.getPropertiesByName();
+                projectToFederated(actualLocation, projection, request, parent, children, properties);
+            } else if (fromSource instanceof ReadBranchRequest) {
+                ReadBranchRequest readFromSource = (ReadBranchRequest)fromSource;
+                for (Location parent : readFromSource) {
+                    List<Location> children = readFromSource.getChildren(parent);
+                    Map<Name, Property> properties = readFromSource.getPropertiesFor(parent);
+                    projectToFederated(actualLocation, projection, request, parent, children, properties);
+                }
+            }
+            setCacheableInfo(request, fromSource.getCachePolicy());
+            projectedRequest = projectedRequest.next();
+            ++numMerged;
+        }
+        if (numMerged == 0) {
+            // No source requests had results ...
+            setPathNotFound(request, request.at(), federatedRequest.getFirstProjectedRequest());
+        } else {
+            request.setActualLocationOfNode(actualLocation);
+        }
+    }
+
+    /**
+     * Project the supplied node information read from a source and update the supplied request.
+     * 
+     * @param ancestorInFederation the federated node under which this information is being projected; may not be null
+     * @param projection the projection used to make the original source request; may not be null
+     * @param request the federated request upon which the results are to be recorded; may not be null
+     * @param parent the location of the parent in the source; may not be null
+     * @param children the location of the children in the source; may be null or empty
+     * @param propertiesByName the properties on the parent in the source; may be null or empty
+     */
+    protected void projectToFederated( Location ancestorInFederation,
+                                       Projection projection,
+                                       ReadBranchRequest request,
+                                       Location parent,
+                                       List<Location> children,
+                                       Map<Name, Property> propertiesByName ) {
+        Path ancestorPath = ancestorInFederation.getPath();
+        if (projection == null) {
+            // This is a placeholder node ...
+            if (children != null) {
+                // Add the children (to any existing children) ...
+                List<Location> existing = request.getChildren(parent);
+                if (existing == null) existing = new ArrayList<Location>(children.size());
+                for (Location child : children) {
+                    existing.add(child);
+                }
+                request.setChildren(parent, existing);
+            }
+            if (propertiesByName != null) {
+                // Add the properties to any existing properties ...
+                Map<Name, Property> propsByName = request.getPropertiesFor(parent);
+                if (propsByName == null) propsByName = new HashMap<Name, Property>();
+                for (Property property : propertiesByName.values()) {
+                    Property existingProperty = propsByName.get(property.getName());
+                    if (existingProperty != null) {
+                        // Merge the property values ...
+                        property = merge(existingProperty, property, propertyFactory, true);
+                    }
+                    propsByName.put(property.getName(), property);
+                }
+                request.setProperties(parent, propsByName.values());
+            }
+            return;
+        }
+        for (Path path : projection.getPathsInRepository(parent.getPath(), pathFactory)) {
+            if (!path.isAtOrBelow(ancestorPath)) continue;
+
+            // Determine the list of children ...
+            Location parentInFederation = parent.with(path);
+            if (children != null) {
+                // Add the children to any existing children ...
+                List<Location> existing = request.getChildren(parentInFederation);
+                if (existing == null) existing = new ArrayList<Location>(children.size());
+                for (Location child : children) {
+                    Path childPath = pathFactory.create(path, child.getPath().getLastSegment());
+                    existing.add(child.with(childPath));
+                }
+                request.setChildren(parentInFederation, existing);
+            }
+
+            // Set or update the properties ...
+            if (propertiesByName != null) {
+                Map<Name, Property> propsByName = request.getPropertiesFor(parentInFederation);
+                if (propsByName == null) propsByName = new HashMap<Name, Property>();
+                for (Property property : propertiesByName.values()) {
+                    Property existingProperty = propsByName.get(property.getName());
+                    if (existingProperty != null) {
+                        // Merge the property values ...
+                        property = merge(existingProperty, property, propertyFactory, true);
+                    }
+                    propsByName.put(property.getName(), property);
+                }
+                request.setProperties(parentInFederation, propsByName.values());
+            }
+            // We're done, since we found a path that's on the ancestor path ...
+            return;
+        }
+    }
+
+    /**
+     * Project the supplied location in a source into its federated location. The projection is used to find the location under
+     * the supplied ancestor. Any errors are recorded on the original request.
+     * 
+     * @param ancestorInFederation the ancestor in the federated repository; may not be nul
+     * @param projection the projection that should be used; may not be null
+     * @param actualSourceLocation the actual location in the source that is to be projected back into the federated repository;
+     *        may not be null
+     * @param originalRequest the original request, if there are errors; may not be null
+     * @return the location in the federated repository
+     */
+    protected Location projectToFederated( Location ancestorInFederation,
+                                           Projection projection,
+                                           Location actualSourceLocation,
+                                           Request originalRequest ) {
+        Path ancestorPath = ancestorInFederation.getPath();
+        Path actualPathInSource = actualSourceLocation.getPath();
+        // Project the actual location ...
+        for (Path path : projection.getPathsInRepository(actualPathInSource, pathFactory)) {
+            if (path.isAtOrBelow(ancestorPath)) {
+                return actualSourceLocation.with(path);
+            }
+        }
+        // Record that there was an error projecting the results ...
+        String whereInSource = actualSourceLocation.getString(getExecutionContext().getNamespaceRegistry());
+        String msg = GraphI18n.unableToProjectSourceInformationIntoWorkspace.text(whereInSource, getSourceName(), projection);
+        originalRequest.setError(new InvalidRequestException(msg));
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
+     */
+    @Override
+    public void process( CreateNodeRequest request ) {
+        ProjectedRequest projected = federatedRequest.getFirstProjectedRequest();
+        assert !projected.hasNext();
+        CreateNodeRequest source = (CreateNodeRequest)projected.getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        Location sourceLocation = source.getActualLocationOfNode();
+        request.setActualLocationOfNode(projectToFederated(request.under(), projected.getProjection(), sourceLocation, request));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
+     */
+    @Override
+    public void process( UpdatePropertiesRequest request ) {
+        ProjectedRequest projected = federatedRequest.getFirstProjectedRequest();
+        assert !projected.hasNext();
+        UpdatePropertiesRequest source = (UpdatePropertiesRequest)projected.getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        Location sourceLocation = source.getActualLocationOfNode();
+        request.setActualLocationOfNode(projectToFederated(request.on(), projected.getProjection(), sourceLocation, request));
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
+     */
+    @Override
+    public void process( DeleteBranchRequest request ) {
+        ProjectedRequest projected = federatedRequest.getFirstProjectedRequest();
+        // Go through the projected requests, and look for the top-most node ...
+        Location highest = null;
+        while (projected != null) {
+            // The projected request should either be a DeleteChildrenRequest (if the node being deleted is
+            // at the top of a projection and therefore required to exist) or a DeleteBranchRequest (in all
+            // other cases)...
+            Location actual = null;
+            Request sourceRequest = projected.getRequest();
+            if (sourceRequest instanceof DeleteBranchRequest) {
+                DeleteBranchRequest source = (DeleteBranchRequest)projected.getRequest();
+                actual = source.getActualLocationOfNode();
+            } else {
+                DeleteChildrenRequest source = (DeleteChildrenRequest)projected.getRequest();
+                actual = source.getActualLocationOfNode();
+            }
+            if (checkErrorOrCancel(request, sourceRequest)) return;
+            if (!projected.isSameLocation() && projected.getProjection() != null) {
+                actual = projectToFederated(request.at(), projected.getProjection(), actual, request);
+            }
+            if (highest == null) highest = actual;
+            else if (highest.getPath().isDecendantOf(actual.getPath())) highest = actual;
+            projected = projected.next();
+        }
+        assert highest != null;
+        request.setActualLocationOfNode(highest);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
+     */
+    @Override
+    public void process( CopyBranchRequest request ) {
+        ProjectedRequest projected = federatedRequest.getFirstProjectedRequest();
+        assert !projected.hasNext();
+        CopyBranchRequest source = (CopyBranchRequest)projected.getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        Location locationBefore = source.getActualLocationBefore();
+        Location locationAfter = source.getActualLocationBefore();
+        locationBefore = projectToFederated(request.from(), projected.getProjection(), locationBefore, request);
+        locationAfter = projectToFederated(request.into(), projected.getSecondProjection(), locationAfter, request);
+        request.setActualLocations(locationBefore, locationAfter);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
+     */
+    @Override
+    public void process( MoveBranchRequest request ) {
+        ProjectedRequest projected = federatedRequest.getFirstProjectedRequest();
+        assert !projected.hasNext();
+        MoveBranchRequest source = (MoveBranchRequest)projected.getRequest();
+        if (checkErrorOrCancel(request, source)) return;
+        Location locationBefore = source.getActualLocationBefore();
+        Location locationAfter = source.getActualLocationBefore();
+        locationBefore = projectToFederated(request.from(), projected.getProjection(), locationBefore, request);
+        locationAfter = projectToFederated(request.into(), projected.getSecondProjection(), locationAfter, request);
+        request.setActualLocations(locationBefore, locationAfter);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
+     */
+    @Override
+    public void process( VerifyWorkspaceRequest request ) {
+        ProjectedRequest projectedRequest = federatedRequest.getFirstProjectedRequest();
+        assert projectedRequest != null;
+        Location actualLocation = Location.create(getExecutionContext().getValueFactories().getPathFactory().createRootPath());
+        while (projectedRequest != null) {
+            VerifyNodeExistsRequest readFromSource = (VerifyNodeExistsRequest)projectedRequest.getRequest();
+            if (readFromSource.hasError()) {
+                request.setError(readFromSource.getError());
+                continue;
+            }
+            request.setError(null);
+            if (readFromSource.isCancelled()) {
+                request.cancel();
+                return;
+            }
+
+            // Accumulate the identification properties ...
+            for (Property propertyInSource : readFromSource.getActualLocationOfNode().getIdProperties()) {
+                Name name = propertyInSource.getName();
+                Property existing = actualLocation.getIdProperty(name);
+                if (existing != null) {
+                    // Merge the property values ...
+                    propertyInSource = merge(existing, propertyInSource, propertyFactory, true);
+                }
+                actualLocation = actualLocation.with(propertyInSource);
+            }
+            projectedRequest = projectedRequest.next();
+        }
+        request.setActualRootLocation(actualLocation);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
+     */
+    @Override
+    public void process( GetWorkspacesRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called, since it's handled in the ForkProcessor
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
+     */
+    @Override
+    public void process( CreateWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called, since it's handled in the ForkProcessor
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
+     */
+    @Override
+    public void process( CloneWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called, since it's handled in the ForkProcessor
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
+     */
+    @Override
+    public void process( DestroyWorkspaceRequest request ) {
+        throw new UnsupportedOperationException(); // should never be called, since it's handled in the ForkProcessor
+    }
+
+    protected boolean checkErrorOrCancel( Request request,
+                                          Request sourceRequest ) {
+        if (sourceRequest.hasError()) {
+            request.setError(sourceRequest.getError());
+            return true;
+        }
+        if (sourceRequest.isCancelled()) {
+            request.cancel();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Merge the values from the two properties with the same name, returning a new property with the newly merged values.
+     * <p>
+     * The current algorithm merges the values by concatenating the values from <code>property1</code> and <code>property2</code>,
+     * and if <code>removeDuplicates</code> is true any values in <code>property2</code> that are identical to values found in
+     * <code>property1</code> are skipped.
+     * </p>
+     * 
+     * @param property1 the first property; may not be null, and must have the same {@link Property#getName() name} as
+     *        <code>property2</code>
+     * @param property2 the second property; may not be null, and must have the same {@link Property#getName() name} as
+     *        <code>property1</code>
+     * @param factory the property factory, used to create the result
+     * @param removeDuplicates true if this method removes any values in the second property that duplicate values found in the
+     *        first property.
+     * @return the property that contains the same {@link Property#getName() name} as the input properties, but with values that
+     *         are merged from both of the input properties
+     */
+    protected Property merge( Property property1,
+                              Property property2,
+                              PropertyFactory factory,
+                              boolean removeDuplicates ) {
+        assert property1 != null;
+        assert property2 != null;
+        assert property1.getName().equals(property2.getName());
+        if (property1.isEmpty()) return property2;
+        if (property2.isEmpty()) return property1;
+
+        // If they are both single-valued, then we can use a more efficient algorithm ...
+        if (property1.isSingle() && property2.isSingle()) {
+            Object value1 = property1.getValues().next();
+            Object value2 = property2.getValues().next();
+            if (removeDuplicates && ValueComparators.OBJECT_COMPARATOR.compare(value1, value2) == 0) return property1;
+            return factory.create(property1.getName(), new Object[] {value1, value2});
+        }
+
+        // One or both properties are multi-valued, so use an algorithm that works with in all cases ...
+        if (!removeDuplicates) {
+            Iterator<?> valueIterator = new DualIterator(property1.getValues(), property2.getValues());
+            return factory.create(property1.getName(), valueIterator);
+        }
+
+        // First copy all the values from property 1 ...
+        Object[] values = new Object[property1.size() + property2.size()];
+        int index = 0;
+        for (Object property1Value : property1) {
+            values[index++] = property1Value;
+        }
+        assert index == property1.size();
+        // Now add any values of property2 that don't match a value in property1 ...
+        for (Object property2Value : property2) {
+            // Brute force, go through the values of property1 and compare ...
+            boolean matched = false;
+            for (Object property1Value : property1) {
+                if (ValueComparators.OBJECT_COMPARATOR.compare(property1Value, property2Value) == 0) {
+                    // The values are the same ...
+                    matched = true;
+                    break;
+                }
+            }
+            if (!matched) values[index++] = property2Value;
+        }
+        if (index != values.length) {
+            Object[] newValues = new Object[index];
+            System.arraycopy(values, 0, newValues, 0, index);
+            values = newValues;
+        }
+        return factory.create(property1.getName(), values);
+    }
+
+    protected static class DualIterator implements Iterator<Object> {
+
+        private final Iterator<?>[] iterators;
+        private Iterator<?> current;
+        private int index = 0;
+
+        protected DualIterator( Iterator<?>... iterators ) {
+            assert iterators != null;
+            assert iterators.length > 0;
+            this.iterators = iterators;
+            this.current = this.iterators[0];
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#hasNext()
+         */
+        public boolean hasNext() {
+            if (this.current != null) return this.current.hasNext();
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#next()
+         */
+        public Object next() {
+            while (this.current != null) {
+                if (this.current.hasNext()) return this.current.next();
+                // Get the next iterator ...
+                if (++this.index < iterators.length) {
+                    this.current = this.iterators[this.index];
+                } else {
+                    this.current = null;
+                }
+            }
+            throw new NoSuchElementException();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#remove()
+         */
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessor.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/MirrorProjector.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/MirrorProjector.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/MirrorProjector.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,83 @@
+/*
+ * 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.connector.federation;
+
+import java.util.List;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+
+/**
+ * A Projector for federated repository configurations that are a direct one-for-one mirror against a single source repository. In
+ * other words, the federated repository has a single projection with a single "/ => /" rule.
+ */
+ at Immutable
+final class MirrorProjector implements Projector {
+
+    /**
+     * Attempt to create an instance of the {@link MirrorProjector} with the supplied projections using the supplied context.
+     * 
+     * @param context the context; may not be null
+     * @param projections the projections in the federated repository; may not be null
+     * @return the mirror projector, or null if the projections didn't match the criteria for such a projector
+     */
+    static MirrorProjector with( ExecutionContext context,
+                                 List<Projection> projections ) {
+        assert projections != null;
+        assert context != null;
+        if (projections.size() != 1) return null;
+        Projection projection = projections.get(0);
+        assert projection != null;
+        if (projection.getRules().size() != 1) return null;
+        PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        List<Path> topLevelPaths = projection.getRules().get(0).getTopLevelPathsInRepository(pathFactory);
+        if (topLevelPaths.size() != 1) return null;
+        if (!topLevelPaths.get(0).isRoot()) return null;
+        return new MirrorProjector(projection);
+    }
+
+    private final Projection projection;
+
+    private MirrorProjector( Projection projection ) {
+        this.projection = projection;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This implementation <i>always<i> returns a single {@link ProxyNode} for the location in the single projection.
+     * </p>
+     * 
+     * @see org.jboss.dna.graph.connector.federation.Projector#project(ExecutionContext, Location, boolean)
+     */
+    public ProjectedNode project( ExecutionContext context,
+                                  Location location,
+                                  boolean requiresUpdate ) {
+        if (requiresUpdate && projection.isReadOnly()) return null;
+        return new ProxyNode(projection, location, location);
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/MirrorProjector.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/NoMoreFederatedRequests.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/NoMoreFederatedRequests.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/NoMoreFederatedRequests.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ *
+ * 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.connector.federation;
+
+/**
+ * Specialization of {@link FederatedRequest}, which is used to place an instance in a queue so the {@link JoinRequestProcessor}
+ * knows that all federated requests have been processed.
+ * 
+ * @see JoinRequestProcessor#process(java.util.concurrent.BlockingQueue)
+ */
+class NoMoreFederatedRequests extends FederatedRequest {
+
+    /**
+     */
+    NoMoreFederatedRequests() {
+        super(null);
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/NoMoreFederatedRequests.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjector.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjector.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjector.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,114 @@
+/*
+ * 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.connector.federation;
+
+import java.util.List;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+
+/**
+ * A Projector for federated repository configurations that are an offset, direct one-for-one mirror against a single source
+ * repository that is projected below the federated root. In other words, the federated repository has a single projection with a
+ * single "/something/below/root => /" rule.
+ */
+ at Immutable
+final class OffsetMirrorProjector extends ProjectorWithPlaceholders {
+
+    /**
+     * Attempt to create an instance of the {@link OffsetMirrorProjector} with the supplied projections using the supplied
+     * context.
+     * 
+     * @param context the context; may not be null
+     * @param projections the projections in the federated repository; may not be null
+     * @return the offset mirror projector, or null if the projections didn't match the criteria for such a projector
+     */
+    static OffsetMirrorProjector with( ExecutionContext context,
+                                       List<Projection> projections ) {
+        assert projections != null;
+        assert context != null;
+        if (projections.size() != 1) return null;
+        Projection projection = projections.get(0);
+        assert projection != null;
+        if (projection.getRules().size() != 1) return null;
+        PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        List<Path> topLevelPaths = projection.getRules().get(0).getTopLevelPathsInRepository(pathFactory);
+        if (topLevelPaths.size() != 1) return null;
+        Path topLevelPath = topLevelPaths.get(0);
+        assert topLevelPath != null;
+        if (topLevelPath.isRoot()) return null;
+        return new OffsetMirrorProjector(context, projections, topLevelPath);
+    }
+
+    private final Projection projection;
+    private final Path offset;
+    private final int offsetSize;
+
+    private OffsetMirrorProjector( ExecutionContext context,
+                                   List<Projection> projections,
+                                   Path offset ) {
+        super(context, projections);
+        this.projection = projections.get(0);
+        this.offset = offset;
+        this.offsetSize = offset.size();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.federation.Projector#project(ExecutionContext, Location, boolean)
+     */
+    public ProjectedNode project( ExecutionContext context,
+                                  Location location,
+                                  boolean requiresUpdate ) {
+        if (requiresUpdate && projection.isReadOnly()) return null;
+        PlaceholderNode placeholder = isPlaceholder(location);
+        if (placeholder != null) return placeholder;
+
+        Path path = location.getPath();
+        Location locationInSource = location;
+        if (path != null) {
+            if (path.size() == offsetSize) {
+                // Make sure the path is the same ...
+                if (path.equals(offset)) {
+                    locationInSource = location.with(context.getValueFactories().getPathFactory().createRootPath());
+                } else {
+                    return null; // not in the path
+                }
+            } else {
+                // Make sure the path begins with the offset ...
+                if (path.isAtOrBelow(offset)) {
+                    locationInSource = location.with(path.subpath(offsetSize));
+                } else {
+                    // Not in the path
+                    return null;
+                }
+            }
+        }
+        return new ProxyNode(projection, locationInSource, location);
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjector.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectedNode.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectedNode.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectedNode.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,200 @@
+/*
+ * 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.connector.federation;
+
+import java.util.List;
+import java.util.Map;
+import net.jcip.annotations.Immutable;
+import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.Property;
+
+/**
+ * Information about a node that has been projected into the repository from a source. Each projected node may be followed by a
+ * {@link #next() next} projected node.
+ */
+ at Immutable
+abstract class ProjectedNode {
+    private final Location location;
+    private ProjectedNode next;
+
+    protected ProjectedNode( Location location ) {
+        this.location = location;
+    }
+
+    public Location location() {
+        return location;
+    }
+
+    public int size() {
+        return next != null ? next.size() + 1 : 1;
+    }
+
+    public int numberOfProxies() {
+        int base = isProxy() ? 1 : 0;
+        return next != null ? next.numberOfProxies() + base : base;
+    }
+
+    public boolean hasNext() {
+        return next != null;
+    }
+
+    public ProjectedNode next() {
+        return next;
+    }
+
+    /*package*/void add( ProjectedNode next ) {
+        if (this.next != null) {
+            this.next.add(next);
+        } else {
+            this.next = next;
+        }
+    }
+
+    public abstract boolean isPlaceholder();
+
+    public abstract boolean isProxy();
+
+    public abstract PlaceholderNode asPlaceholder();
+
+    public abstract ProxyNode asProxy();
+}
+
+ at Immutable
+class PlaceholderNode extends ProjectedNode {
+    private final Map<Name, Property> properties;
+    private final List<Location> children;
+
+    protected PlaceholderNode( Location location,
+                               Map<Name, Property> properties,
+                               List<Location> children ) {
+        super(location);
+        this.properties = properties;
+        this.children = children;
+    }
+
+    public List<Location> children() {
+        return children;
+    }
+
+    public Map<Name, Property> properties() {
+        return properties;
+    }
+
+    @Override
+    public boolean isPlaceholder() {
+        return true;
+    }
+
+    @Override
+    public PlaceholderNode asPlaceholder() {
+        return this;
+    }
+
+    @Override
+    public boolean isProxy() {
+        return false;
+    }
+
+    @Override
+    public ProxyNode asProxy() {
+        return null;
+    }
+}
+
+ at NotThreadSafe
+class ProxyNode extends ProjectedNode {
+    private final Projection projection;
+    private final boolean sameLocationAsOriginal;
+    private final Location federatedLocation;
+
+    protected ProxyNode( Projection projection,
+                         Location locationInSource,
+                         Location locationInFederated ) {
+        super(locationInSource);
+        this.projection = projection;
+        this.federatedLocation = locationInFederated;
+        this.sameLocationAsOriginal = locationInSource.equals(locationInFederated);
+    }
+
+    protected ProxyNode( Projection projection,
+                         Location locationInSource,
+                         Location locationInFederated,
+                         boolean isSameLocation ) {
+        super(locationInSource);
+        this.projection = projection;
+        this.federatedLocation = locationInFederated;
+        this.sameLocationAsOriginal = isSameLocation;
+    }
+
+    public String source() {
+        return projection.getSourceName();
+    }
+
+    public String workspaceName() {
+        return projection.getWorkspaceName();
+    }
+
+    /*package*/Projection projection() {
+        return projection;
+    }
+
+    public Location federatedLocation() {
+        return federatedLocation;
+    }
+
+    @Override
+    public boolean isPlaceholder() {
+        return false;
+    }
+
+    @Override
+    public PlaceholderNode asPlaceholder() {
+        return null;
+    }
+
+    @Override
+    public boolean isProxy() {
+        return true;
+    }
+
+    @Override
+    public ProxyNode asProxy() {
+        return this;
+    }
+
+    /**
+     * Determine whether this projected node is a top-level node in the source projected into the federated repository.
+     * 
+     * @return true if the projected node is a top-level node in the source, or false otherwise
+     */
+    public boolean isTopLevelNode() {
+        return federatedLocation != null && federatedLocation.hasPath() && projection.isTopLevelPath(federatedLocation.getPath());
+    }
+
+    public boolean isSameLocationAsOriginal() {
+        return this.sameLocationAsOriginal;
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectedNode.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projection.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projection.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projection.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,887 @@
+/*
+ * 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.connector.federation;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.text.TextEncoder;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.connector.RepositorySource;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+
+/**
+ * A projection of content from a source into the integrated/federated repository. Each project consists of a set of {@link Rule
+ * rules} for a particular source, where each rule defines how content within a source is
+ * {@link Rule#getPathInRepository(Path, PathFactory) is project into the repository} and how the repository content is
+ * {@link Rule#getPathInSource(Path, PathFactory) projected into the source}. Different rule subclasses are used for different
+ * types.
+ */
+ at Immutable
+public class Projection implements Comparable<Projection>, Serializable {
+
+    /**
+     * Initial version
+     */
+    private static final long serialVersionUID = 1L;
+    protected static final List<Method> parserMethods;
+    static {
+        parserMethods = new CopyOnWriteArrayList<Method>();
+        try {
+            parserMethods.add(Projection.class.getDeclaredMethod("parsePathRule", String.class, ExecutionContext.class));
+        } catch (Throwable err) {
+            Logger.getLogger(Projection.class).error(err, GraphI18n.errorAddingProjectionRuleParseMethod);
+        }
+    }
+
+    /**
+     * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
+     * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
+     * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
+     * null if the definition format could not be understood by the method. Any exceptions during
+     * {@link Method#invoke(Object, Object...) invocation} will be logged at the
+     * {@link Logger#trace(Throwable, String, Object...) trace} level.
+     * 
+     * @param method the method to be added
+     * @see #addRuleParser(ClassLoader, String, String)
+     */
+    public static void addRuleParser( Method method ) {
+        if (method != null) parserMethods.add(method);
+    }
+
+    /**
+     * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
+     * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
+     * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
+     * null if the definition format could not be understood by the method. Any exceptions during
+     * {@link Method#invoke(Object, Object...) invocation} will be logged at the
+     * {@link Logger#trace(Throwable, String, Object...) trace} level.
+     * 
+     * @param classLoader the class loader that should be used to load the class on which the method is defined; may not be null
+     * @param className the name of the class on which the static method is defined; may not be null
+     * @param methodName the name of the method
+     * @throws SecurityException if there is a security exception while loading the class or getting the method
+     * @throws NoSuchMethodException if the method does not exist on the class
+     * @throws ClassNotFoundException if the class could not be found given the supplied class loader
+     * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
+     *         empty
+     * @see #addRuleParser(Method)
+     */
+    public static void addRuleParser( ClassLoader classLoader,
+                                      String className,
+                                      String methodName ) throws SecurityException, NoSuchMethodException, ClassNotFoundException {
+        CheckArg.isNotNull(classLoader, "classLoader");
+        CheckArg.isNotEmpty(className, "className");
+        CheckArg.isNotEmpty(methodName, "methodName");
+        Class<?> clazz = Class.forName(className, true, classLoader);
+        parserMethods.add(clazz.getMethod(className, String.class, ExecutionContext.class));
+    }
+
+    /**
+     * Remove the rule parser method.
+     * 
+     * @param method the method to remove
+     * @return true if the method was removed, or false if the method was not a registered rule parser method
+     */
+    public static boolean removeRuleParser( Method method ) {
+        return parserMethods.remove(method);
+    }
+
+    /**
+     * Remove the rule parser method.
+     * 
+     * @param declaringClassName the name of the class on which the static method is defined; may not be null
+     * @param methodName the name of the method
+     * @return true if the method was removed, or false if the method was not a registered rule parser method
+     * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
+     *         empty
+     */
+    public static boolean removeRuleParser( String declaringClassName,
+                                            String methodName ) {
+        CheckArg.isNotEmpty(declaringClassName, "declaringClassName");
+        CheckArg.isNotEmpty(methodName, "methodName");
+        for (Method method : parserMethods) {
+            if (method.getName().equals(methodName) && method.getDeclaringClass().getName().equals(declaringClassName)) {
+                return parserMethods.remove(method);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Parse the string form of a rule definition and return the rule
+     * 
+     * @param definition the definition of the rule that is to be parsed
+     * @param context the environment in which this method is being executed; may not be null
+     * @return the rule, or null if the definition could not be parsed
+     */
+    public static Rule fromString( String definition,
+                                   ExecutionContext context ) {
+        CheckArg.isNotNull(context, "env");
+        definition = definition != null ? definition.trim() : "";
+        if (definition.length() == 0) return null;
+        for (Method method : parserMethods) {
+            try {
+                Rule rule = (Rule)method.invoke(null, definition, context);
+                if (rule != null) return rule;
+            } catch (Throwable err) {
+                String msg = "Error while parsing project rule definition \"{0}\" using {1}";
+                context.getLogger(Projection.class).trace(err, msg, definition, method);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Pattern that identifies the form:
+     * 
+     * <pre>
+     *    repository_path =&gt; source_path [$ exception ]*
+     * </pre>
+     * 
+     * where the following groups are captured on the first call to {@link Matcher#find()}:
+     * <ol>
+     * <li><code>repository_path</code></li>
+     * <li><code>source_path</code></li>
+     * </ol>
+     * and the following groups are captured on subsequent calls to {@link Matcher#find()}:
+     * <ol>
+     * <li>exception</code></li>
+     * </ol>
+     * <p>
+     * The regular expression is:
+     * 
+     * <pre>
+     * ((?:[&circ;=$]|=(?!&gt;))+)(?:(?:=&gt;((?:[&circ;=$]|=(?!&gt;))+))( \$ (?:(?:[&circ;=]|=(?!&gt;))+))*)?
+     * </pre>
+     * 
+     * </p>
+     */
+    protected static final String PATH_RULE_PATTERN_STRING = "((?:[^=$]|=(?!>))+)(?:(?:=>((?:[^=$]|=(?!>))+))( \\$ (?:(?:[^=]|=(?!>))+))*)?";
+    protected static final Pattern PATH_RULE_PATTERN = Pattern.compile(PATH_RULE_PATTERN_STRING);
+
+    /**
+     * Parse the string definition of a {@link PathRule}. This method is automatically registered in the {@link #parserMethods
+     * parser methods} by the static initializer of {@link Projection}.
+     * 
+     * @param definition the definition
+     * @param context the environment
+     * @return the path rule, or null if the definition is not in the right form
+     */
+    public static PathRule parsePathRule( String definition,
+                                          ExecutionContext context ) {
+        definition = definition != null ? definition.trim() : "";
+        if (definition.length() == 0) return null;
+        Matcher matcher = PATH_RULE_PATTERN.matcher(definition);
+        if (!matcher.find()) return null;
+        String reposPathStr = matcher.group(1);
+        String sourcePathStr = matcher.group(2);
+        if (reposPathStr == null || sourcePathStr == null) return null;
+        reposPathStr = reposPathStr.trim();
+        sourcePathStr = sourcePathStr.trim();
+        if (reposPathStr.length() == 0 || sourcePathStr.length() == 0) return null;
+        PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        Path repositoryPath = pathFactory.create(reposPathStr);
+        Path sourcePath = pathFactory.create(sourcePathStr);
+
+        // Grab the exceptions ...
+        List<Path> exceptions = new LinkedList<Path>();
+        while (matcher.find()) {
+            String exceptionStr = matcher.group(1);
+            Path exception = pathFactory.create(exceptionStr);
+            exceptions.add(exception);
+        }
+        return new PathRule(repositoryPath, sourcePath, exceptions);
+    }
+
+    private final String sourceName;
+    private final String workspaceName;
+    private final List<Rule> rules;
+    private final boolean simple;
+    private final boolean readOnly;
+    private final int hc;
+
+    /**
+     * Create a new federated projection for the supplied source, using the supplied rules.
+     * 
+     * @param sourceName the name of the source
+     * @param workspaceName the name of the workspace in the source; may be null if the default workspace is to be used
+     * @param readOnly true if this projection is considered read-only, or false if the content of the projection may be modified
+     *        by the federated clients
+     * @param rules the projection rules
+     * @throws IllegalArgumentException if the source name or rule array is null, empty, or contains all nulls
+     */
+    public Projection( String sourceName,
+                       String workspaceName,
+                       boolean readOnly,
+                       Rule... rules ) {
+        CheckArg.isNotEmpty(sourceName, "sourceName");
+        CheckArg.isNotEmpty(rules, "rules");
+        this.sourceName = sourceName;
+        this.workspaceName = workspaceName;
+        List<Rule> rulesList = new ArrayList<Rule>();
+        for (Rule rule : rules) {
+            if (rule != null) rulesList.add(rule);
+        }
+        this.readOnly = readOnly;
+        this.rules = Collections.unmodifiableList(rulesList);
+        CheckArg.isNotEmpty(this.rules, "rules");
+        this.simple = computeSimpleProjection(this.rules);
+        this.hc = HashCode.compute(this.sourceName, this.workspaceName);
+    }
+
+    /**
+     * Get the name of the source to which this projection applies.
+     * 
+     * @return the source name
+     * @see RepositorySource#getName()
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * Get the name of the workspace in the source to which this projection applies.
+     * 
+     * @return the workspace name, or null if the default workspace of the {@link #getSourceName() source} is to be used
+     */
+    public String getWorkspaceName() {
+        return workspaceName;
+    }
+
+    /**
+     * Get the rules that define this projection.
+     * 
+     * @return the unmodifiable list of immutable rules; never null
+     */
+    public List<Rule> getRules() {
+        return rules;
+    }
+
+    /**
+     * Get the paths in the source that correspond to the supplied path within the repository. This method computes the paths
+     * given all of the rules. In general, most sources will probably project a node onto a single repository node. However, some
+     * sources may be configured such that the same node in the repository is a projection of multiple nodes within the source.
+     * 
+     * @param canonicalPathInRepository the canonical path of the node within the repository; may not be null
+     * @param factory the path factory; may not be null
+     * @return the set of unique paths in the source projected from the repository path; never null
+     * @throws IllegalArgumentException if the factory reference is null
+     */
+    public Set<Path> getPathsInSource( Path canonicalPathInRepository,
+                                       PathFactory factory ) {
+        CheckArg.isNotNull(factory, "factory");
+        assert canonicalPathInRepository == null ? true : canonicalPathInRepository.equals(canonicalPathInRepository.getCanonicalPath());
+        Set<Path> paths = new HashSet<Path>();
+        for (Rule rule : getRules()) {
+            Path pathInSource = rule.getPathInSource(canonicalPathInRepository, factory);
+            if (pathInSource != null) paths.add(pathInSource);
+        }
+        return paths;
+    }
+
+    /**
+     * Get the paths in the repository that correspond to the supplied path within the source. This method computes the paths
+     * given all of the rules. In general, most sources will probably project a node onto a single repository node. However, some
+     * sources may be configured such that the same node in the source is projected into multiple nodes within the repository.
+     * 
+     * @param canonicalPathInSource the canonical path of the node within the source; may not be null
+     * @param factory the path factory; may not be null
+     * @return the set of unique paths in the repository projected from the source path; never null
+     * @throws IllegalArgumentException if the factory reference is null
+     */
+    public Set<Path> getPathsInRepository( Path canonicalPathInSource,
+                                           PathFactory factory ) {
+        CheckArg.isNotNull(factory, "factory");
+        assert canonicalPathInSource == null ? true : canonicalPathInSource.equals(canonicalPathInSource.getCanonicalPath());
+        Set<Path> paths = new HashSet<Path>();
+        for (Rule rule : getRules()) {
+            Path pathInRepository = rule.getPathInRepository(canonicalPathInSource, factory);
+            if (pathInRepository != null) paths.add(pathInRepository);
+        }
+        return paths;
+    }
+
+    /**
+     * Get the paths in the repository that serve as top-level nodes exposed by this projection.
+     * 
+     * @param factory the path factory that can be used to create new paths; may not be null
+     * @return the list of top-level paths, in the proper order and containing no duplicates; never null
+     */
+    public List<Path> getTopLevelPathsInRepository( PathFactory factory ) {
+        CheckArg.isNotNull(factory, "factory");
+        List<Rule> rules = getRules();
+        Set<Path> uniquePaths = new HashSet<Path>();
+        List<Path> paths = new ArrayList<Path>(rules.size());
+        for (Rule rule : getRules()) {
+            for (Path path : rule.getTopLevelPathsInRepository(factory)) {
+                if (!uniquePaths.contains(path)) {
+                    paths.add(path);
+                    uniquePaths.add(path);
+                }
+            }
+        }
+        return paths;
+    }
+
+    /**
+     * Determine whether the supplied repositoryPath is considered one of the top-level nodes in this projection.
+     * 
+     * @param repositoryPath path in the repository; may not be null
+     * @return true if the supplied repository path is one of the top-level nodes exposed by this projection, or false otherwise
+     */
+    public boolean isTopLevelPath( Path repositoryPath ) {
+        for (Rule rule : getRules()) {
+            if (rule.isTopLevelPath(repositoryPath)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Determine whether this project is a simple projection that only involves for any one repository path no more than a single
+     * source path.
+     * 
+     * @return true if this projection is a simple projection, or false if the projection is not simple (or it cannot be
+     *         determined if it is simple)
+     */
+    public boolean isSimple() {
+        return simple;
+    }
+
+    /**
+     * Determine whether the content projected by this projection is read-only.
+     * 
+     * @return true if the content is read-only, or false if it can be modified
+     */
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
+    protected boolean computeSimpleProjection( List<Rule> rules ) {
+        // Get the set of repository paths for the rules, and see if they overlap ...
+        Set<Path> repositoryPaths = new HashSet<Path>();
+        for (Rule rule : rules) {
+            if (rule instanceof PathRule) {
+                PathRule pathRule = (PathRule)rule;
+                Path repoPath = pathRule.getPathInRepository();
+                if (!repositoryPaths.isEmpty()) {
+                    if (repositoryPaths.contains(repoPath)) return false;
+                    for (Path path : repositoryPaths) {
+                        if (path.isAtOrAbove(repoPath)) return false;
+                        if (repoPath.isAtOrAbove(path)) return false;
+                    }
+                }
+                repositoryPaths.add(repoPath);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return this.hc;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof Projection) {
+            Projection that = (Projection)obj;
+            if (this.hashCode() != that.hashCode()) return false;
+            if (!this.getSourceName().equals(that.getSourceName())) return false;
+            if (!this.getWorkspaceName().equals(that.getWorkspaceName())) return false;
+            if (!this.getRules().equals(that.getRules())) return false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo( Projection that ) {
+        if (this == that) return 0;
+        int diff = this.getSourceName().compareTo(that.getSourceName());
+        if (diff != 0) return diff;
+        diff = this.getWorkspaceName().compareTo(that.getWorkspaceName());
+        if (diff != 0) return diff;
+        Iterator<Rule> thisIter = this.getRules().iterator();
+        Iterator<Rule> thatIter = that.getRules().iterator();
+        while (thisIter.hasNext() && thatIter.hasNext()) {
+            diff = thisIter.next().compareTo(thatIter.next());
+            if (diff != 0) return diff;
+        }
+        if (thisIter.hasNext()) return 1;
+        if (thatIter.hasNext()) return -1;
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(this.sourceName);
+        sb.append("::");
+        sb.append(this.workspaceName);
+        sb.append(" { ");
+        boolean first = true;
+        for (Rule rule : this.getRules()) {
+            if (!first) sb.append(" ; ");
+            sb.append(rule.toString());
+            first = false;
+        }
+        sb.append(" }");
+        return sb.toString();
+    }
+
+    /**
+     * A rule used within a project do define how content within a source is projected into the federated repository. This mapping
+     * is bi-directional, meaning it's possible to determine
+     * <ul>
+     * <li>the path in repository given a path in source; and</li>
+     * <li>the path in source given a path in repository.</li>
+     * </ul>
+     * 
+     * @author Randall Hauch
+     */
+    @Immutable
+    public static abstract class Rule implements Comparable<Rule> {
+
+        /**
+         * Get the paths in the repository that serve as top-level nodes exposed by this rule.
+         * 
+         * @param factory the path factory that can be used to create new paths; may not be null
+         * @return the list of top-level paths, which are ordered and which must be unique; never null
+         */
+        public abstract List<Path> getTopLevelPathsInRepository( PathFactory factory );
+
+        /**
+         * Determine if the supplied path is the same as one of the top-level nodes exposed by this rule.
+         * 
+         * @param path the path; may not be null
+         * @return true if the supplied path is also one of the {@link #getTopLevelPathsInRepository(PathFactory) top-level paths}
+         *         , or false otherwise
+         */
+        public abstract boolean isTopLevelPath( Path path );
+
+        /**
+         * Get the path in source that is projected from the supplied repository path, or null if the supplied repository path is
+         * not projected into the source.
+         * 
+         * @param pathInRepository the path in the repository; may not be null
+         * @param factory the path factory; may not be null
+         * @return the path in source if it is projected by this rule, or null otherwise
+         */
+        public abstract Path getPathInSource( Path pathInRepository,
+                                              PathFactory factory );
+
+        /**
+         * Get the path in repository that is projected from the supplied source path, or null if the supplied source path is not
+         * projected into the repository.
+         * 
+         * @param pathInSource the path in the source; may not be null
+         * @param factory the path factory; may not be null
+         * @return the path in repository if it is projected by this rule, or null otherwise
+         */
+        public abstract Path getPathInRepository( Path pathInSource,
+                                                  PathFactory factory );
+
+        public abstract String getString( NamespaceRegistry registry,
+                                          TextEncoder encoder );
+
+        public abstract String getString( TextEncoder encoder );
+
+        public abstract String getString();
+    }
+
+    /**
+     * A rule that is defined with a single {@link #getPathInSource() path in source} and a single {@link #getPathInRepository()
+     * path in repository}, and which has a set of {@link #getExceptionsToRule() path exceptions} (relative paths below the path
+     * in source).
+     * 
+     * @author Randall Hauch
+     */
+    @Immutable
+    public static class PathRule extends Rule {
+        /** The path of the content as known to the source */
+        private final Path sourcePath;
+        /** The path where the content is to be placed ("projected") into the repository */
+        private final Path repositoryPath;
+        /** The paths (relative to the source path) that identify exceptions to this rule */
+        private final List<Path> exceptions;
+        private final int hc;
+        private final List<Path> topLevelRepositoryPaths;
+
+        public PathRule( Path repositoryPath,
+                         Path sourcePath ) {
+            this(repositoryPath, sourcePath, (Path[])null);
+        }
+
+        public PathRule( Path repositoryPath,
+                         Path sourcePath,
+                         Path... exceptions ) {
+            assert sourcePath != null;
+            assert repositoryPath != null;
+            this.sourcePath = sourcePath;
+            this.repositoryPath = repositoryPath;
+            if (exceptions == null || exceptions.length == 0) {
+                this.exceptions = Collections.emptyList();
+            } else {
+                List<Path> exceptionList = new ArrayList<Path>();
+                for (Path exception : exceptions) {
+                    if (exception != null) exceptionList.add(exception);
+                }
+                this.exceptions = Collections.unmodifiableList(exceptionList);
+            }
+            this.hc = HashCode.compute(sourcePath, repositoryPath, exceptions);
+            assert exceptionPathsAreRelative();
+            this.topLevelRepositoryPaths = Collections.singletonList(getPathInRepository());
+        }
+
+        public PathRule( Path repositoryPath,
+                         Path sourcePath,
+                         List<Path> exceptions ) {
+            assert sourcePath != null;
+            assert repositoryPath != null;
+            this.sourcePath = sourcePath;
+            this.repositoryPath = repositoryPath;
+            if (exceptions == null || exceptions.isEmpty()) {
+                this.exceptions = Collections.emptyList();
+            } else {
+                this.exceptions = Collections.unmodifiableList(new ArrayList<Path>(exceptions));
+            }
+            this.hc = HashCode.compute(sourcePath, repositoryPath, exceptions);
+            assert exceptionPathsAreRelative();
+            this.topLevelRepositoryPaths = Collections.singletonList(getPathInRepository());
+        }
+
+        private boolean exceptionPathsAreRelative() {
+            if (this.exceptions != null) {
+                for (Path path : this.exceptions) {
+                    if (path.isAbsolute()) return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * The path where the content is to be placed ("projected") into the repository.
+         * 
+         * @return the projected path of the content in the repository; never null
+         */
+        public Path getPathInRepository() {
+            return repositoryPath;
+        }
+
+        /**
+         * The path of the content as known to the source
+         * 
+         * @return the source-specific path of the content; never null
+         */
+        public Path getPathInSource() {
+            return sourcePath;
+        }
+
+        /**
+         * Get whether this rule has any exceptions.
+         * 
+         * @return true if this rule has exceptions, or false if it has none.
+         */
+        public boolean hasExceptionsToRule() {
+            return exceptions.size() != 0;
+        }
+
+        /**
+         * Get the paths that define the exceptions to this rule. These paths are always relative to the
+         * {@link #getPathInSource() path in source}.
+         * 
+         * @return the unmodifiable exception paths; never null but possibly empty
+         */
+        public List<Path> getExceptionsToRule() {
+            return exceptions;
+        }
+
+        /**
+         * @param pathInSource
+         * @return true if the source path is included by this rule
+         */
+        protected boolean includes( Path pathInSource ) {
+            // Check whether the path is outside the source-specific path ...
+            if (pathInSource != null && this.sourcePath.isAtOrAbove(pathInSource)) {
+
+                // The path is inside the source-specific region, so check the exceptions ...
+                List<Path> exceptions = getExceptionsToRule();
+                if (exceptions.size() != 0) {
+                    Path subpathInSource = pathInSource.relativeTo(this.sourcePath);
+                    if (subpathInSource.size() != 0) {
+                        for (Path exception : exceptions) {
+                            if (subpathInSource.isAtOrBelow(exception)) return false;
+                        }
+                    }
+                }
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Rule#getTopLevelPathsInRepository(org.jboss.dna.graph.property.PathFactory)
+         */
+        @Override
+        public List<Path> getTopLevelPathsInRepository( PathFactory factory ) {
+            return topLevelRepositoryPaths;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.connector.federation.Projection.Rule#isTopLevelPath(org.jboss.dna.graph.property.Path)
+         */
+        @Override
+        public boolean isTopLevelPath( Path path ) {
+            for (Path topLevel : topLevelRepositoryPaths) {
+                if (topLevel.equals(path)) return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * This method considers a path that is at or below the rule's {@link #getPathInSource() source path} to be included,
+         * except if there are {@link #getExceptionsToRule() exceptions} that explicitly disallow the path.
+         * </p>
+         * 
+         * @see Rule#getPathInSource(Path, PathFactory)
+         */
+        @Override
+        public Path getPathInSource( Path pathInRepository,
+                                     PathFactory factory ) {
+            assert pathInRepository.equals(pathInRepository.getCanonicalPath());
+            // Project the repository path into the equivalent source path ...
+            Path pathInSource = projectPathInRepositoryToPathInSource(pathInRepository, factory);
+
+            // Check whether the source path is included by this rule ...
+            return includes(pathInSource) ? pathInSource : null;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Rule#getPathInRepository(org.jboss.dna.graph.property.Path, org.jboss.dna.graph.property.PathFactory)
+         */
+        @Override
+        public Path getPathInRepository( Path pathInSource,
+                                         PathFactory factory ) {
+            assert pathInSource.equals(pathInSource.getCanonicalPath());
+            // Check whether the source path is included by this rule ...
+            if (!includes(pathInSource)) return null;
+
+            // Project the repository path into the equivalent source path ...
+            return projectPathInSourceToPathInRepository(pathInSource, factory);
+        }
+
+        /**
+         * Convert a path defined in the source system into an equivalent path in the repository system.
+         * 
+         * @param pathInSource the path in the source system, which may include the {@link #getPathInSource()}
+         * @param factory the path factory; may not be null
+         * @return the path in the repository system, which will be normalized and absolute (including the
+         *         {@link #getPathInRepository()}), or null if the path is not at or under the {@link #getPathInSource()}
+         */
+        protected Path projectPathInSourceToPathInRepository( Path pathInSource,
+                                                              PathFactory factory ) {
+            if (!this.sourcePath.isAtOrAbove(pathInSource)) return null;
+            // Remove the leading source path ...
+            Path relativeSourcePath = pathInSource.relativeTo(this.sourcePath);
+            // Prepend the region's root path ...
+            Path result = factory.create(this.repositoryPath, relativeSourcePath);
+            return result.getNormalizedPath();
+        }
+
+        /**
+         * Convert a path defined in the repository system into an equivalent path in the source system.
+         * 
+         * @param pathInRepository the path in the repository system, which may include the {@link #getPathInRepository()}
+         * @param factory the path factory; may not be null
+         * @return the path in the source system, which will be normalized and absolute (including the {@link #getPathInSource()}
+         *         ), or null if the path is not at or under the {@link #getPathInRepository()}
+         */
+        protected Path projectPathInRepositoryToPathInSource( Path pathInRepository,
+                                                              PathFactory factory ) {
+            if (!this.repositoryPath.isAtOrAbove(pathInRepository)) return null;
+            // Find the relative path from the root of this region ...
+            Path pathInRegion = pathInRepository.relativeTo(this.repositoryPath);
+            // Prepend the path in source ...
+            Path result = factory.create(this.sourcePath, pathInRegion);
+            return result.getNormalizedPath();
+        }
+
+        @Override
+        public String getString( NamespaceRegistry registry,
+                                 TextEncoder encoder ) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(this.getPathInRepository().getString(registry, encoder));
+            sb.append(" => ");
+            sb.append(this.getPathInSource().getString(registry, encoder));
+            if (this.getExceptionsToRule().size() != 0) {
+                for (Path exception : this.getExceptionsToRule()) {
+                    sb.append(" $ ");
+                    sb.append(exception.getString(registry, encoder));
+                }
+            }
+            return sb.toString();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Rule#getString(org.jboss.dna.common.text.TextEncoder)
+         */
+        @Override
+        public String getString( TextEncoder encoder ) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(this.getPathInRepository().getString(encoder));
+            sb.append(" => ");
+            sb.append(this.getPathInSource().getString(encoder));
+            if (this.getExceptionsToRule().size() != 0) {
+                for (Path exception : this.getExceptionsToRule()) {
+                    sb.append(" $ ");
+                    sb.append(exception.getString(encoder));
+                }
+            }
+            return sb.toString();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Rule#getString()
+         */
+        @Override
+        public String getString() {
+            return getString(Path.JSR283_ENCODER);
+        }
+
+        /**
+         * {@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 PathRule) {
+                PathRule that = (PathRule)obj;
+                if (!this.getPathInRepository().equals(that.getPathInRepository())) return false;
+                if (!this.getPathInSource().equals(that.getPathInSource())) return false;
+                if (!this.getExceptionsToRule().equals(that.getExceptionsToRule())) return false;
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Comparable#compareTo(java.lang.Object)
+         */
+        public int compareTo( Rule other ) {
+            if (other == this) return 0;
+            if (other instanceof PathRule) {
+                PathRule that = (PathRule)other;
+                int diff = this.getPathInRepository().compareTo(that.getPathInRepository());
+                if (diff != 0) return diff;
+                diff = this.getPathInSource().compareTo(that.getPathInSource());
+                if (diff != 0) return diff;
+                Iterator<Path> thisIter = this.getExceptionsToRule().iterator();
+                Iterator<Path> thatIter = that.getExceptionsToRule().iterator();
+                while (thisIter.hasNext() && thatIter.hasNext()) {
+                    diff = thisIter.next().compareTo(thatIter.next());
+                    if (diff != 0) return diff;
+                }
+                if (thisIter.hasNext()) return 1;
+                if (thatIter.hasNext()) return -1;
+                return 0;
+            }
+            return other.getClass().getName().compareTo(this.getClass().getName());
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#toString()
+         */
+        @Override
+        public String toString() {
+            return getString();
+        }
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projection.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectionParser.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectionParser.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectionParser.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,245 @@
+/*
+ * 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.connector.federation;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.text.TextEncoder;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.common.util.StringUtil;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+
+/**
+ * A parser library for {@link Projection projections} and {@link Projection.Rule projection rules}.
+ */
+ at ThreadSafe
+public class ProjectionParser {
+    private static final ProjectionParser INSTANCE;
+
+    static {
+        INSTANCE = new ProjectionParser();
+        try {
+            INSTANCE.addRuleParser(Projection.class, "parsePathRule");
+            assert INSTANCE.parserMethods.size() == 1;
+        } catch (Throwable err) {
+            Logger.getLogger(Projection.class).error(err, GraphI18n.errorAddingProjectionRuleParseMethod);
+        }
+    }
+
+    /**
+     * Get the shared projection parser, which is by default populated with the standard parser rules.
+     * 
+     * @return the parser; never null
+     */
+    public static ProjectionParser getInstance() {
+        return INSTANCE;
+    }
+
+    private final List<Method> parserMethods = new CopyOnWriteArrayList<Method>();
+
+    public ProjectionParser() {
+    }
+
+    /**
+     * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
+     * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
+     * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
+     * null if the definition format could not be understood by the method. Any exceptions during
+     * {@link Method#invoke(Object, Object...) invocation} will be logged at the
+     * {@link Logger#trace(Throwable, String, Object...) trace} level.
+     * 
+     * @param method the method to be added
+     * @see #addRuleParser(ClassLoader, String, String)
+     */
+    public void addRuleParser( Method method ) {
+        if (method != null) parserMethods.add(method);
+    }
+
+    /**
+     * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
+     * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
+     * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
+     * null if the definition format could not be understood by the method. Any exceptions during
+     * {@link Method#invoke(Object, Object...) invocation} will be logged at the
+     * {@link Logger#trace(Throwable, String, Object...) trace} level.
+     * 
+     * @param clazz the class on which the static method is defined; may not be null
+     * @param methodName the name of the method
+     * @throws SecurityException if there is a security exception while loading the class or getting the method
+     * @throws NoSuchMethodException if the method does not exist on the class
+     * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
+     *         empty
+     * @see #addRuleParser(Method)
+     */
+    public void addRuleParser( Class<?> clazz,
+                               String methodName ) throws SecurityException, NoSuchMethodException {
+        CheckArg.isNotNull(clazz, "clazz");
+        CheckArg.isNotEmpty(methodName, "methodName");
+        parserMethods.add(clazz.getMethod(methodName, String.class, ExecutionContext.class));
+    }
+
+    /**
+     * Add a static method that can be used to parse {@link Rule#getString(NamespaceRegistry, TextEncoder) rule definition
+     * strings}. These methods must be static, must accept a {@link String} definition as the first parameter and an
+     * {@link ExecutionContext} environment reference as the second parameter, and should return the resulting {@link Rule} (or
+     * null if the definition format could not be understood by the method. Any exceptions during
+     * {@link Method#invoke(Object, Object...) invocation} will be logged at the
+     * {@link Logger#trace(Throwable, String, Object...) trace} level.
+     * 
+     * @param classLoader the class loader that should be used to load the class on which the method is defined; may not be null
+     * @param className the name of the class on which the static method is defined; may not be null
+     * @param methodName the name of the method
+     * @throws SecurityException if there is a security exception while loading the class or getting the method
+     * @throws NoSuchMethodException if the method does not exist on the class
+     * @throws ClassNotFoundException if the class could not be found given the supplied class loader
+     * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
+     *         empty
+     * @see #addRuleParser(Method)
+     */
+    public void addRuleParser( ClassLoader classLoader,
+                               String className,
+                               String methodName ) throws SecurityException, NoSuchMethodException, ClassNotFoundException {
+        CheckArg.isNotNull(classLoader, "classLoader");
+        CheckArg.isNotEmpty(className, "className");
+        CheckArg.isNotEmpty(methodName, "methodName");
+        Class<?> clazz = Class.forName(className, true, classLoader);
+        parserMethods.add(clazz.getMethod(methodName, String.class, ExecutionContext.class));
+    }
+
+    /**
+     * Remove the rule parser method.
+     * 
+     * @param method the method to remove
+     * @return true if the method was removed, or false if the method was not a registered rule parser method
+     */
+    public boolean removeRuleParser( Method method ) {
+        return parserMethods.remove(method);
+    }
+
+    /**
+     * Remove the rule parser method.
+     * 
+     * @param declaringClassName the name of the class on which the static method is defined; may not be null
+     * @param methodName the name of the method
+     * @return true if the method was removed, or false if the method was not a registered rule parser method
+     * @throws IllegalArgumentException if the class loader reference is null, or if the class name or method name are null or
+     *         empty
+     */
+    public boolean removeRuleParser( String declaringClassName,
+                                     String methodName ) {
+        CheckArg.isNotEmpty(declaringClassName, "declaringClassName");
+        CheckArg.isNotEmpty(methodName, "methodName");
+        for (Method method : parserMethods) {
+            if (method.getName().equals(methodName) && method.getDeclaringClass().getName().equals(declaringClassName)) {
+                return parserMethods.remove(method);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return parserMethods
+     */
+    /*package*/List<Method> getParserMethods() {
+        return Collections.unmodifiableList(parserMethods);
+    }
+
+    /**
+     * Parse the string form of a rule definition and return the rule
+     * 
+     * @param definition the definition of the rule that is to be parsed
+     * @param context the environment in which this method is being executed; may not be null
+     * @return the rule, or null if the definition could not be parsed
+     */
+    public Rule ruleFromString( String definition,
+                                ExecutionContext context ) {
+        CheckArg.isNotNull(context, "env");
+        definition = definition != null ? definition.trim() : "";
+        if (definition.length() == 0) return null;
+        Logger logger = context.getLogger(getClass());
+        for (Method method : parserMethods) {
+            try {
+                Rule rule = (Rule)method.invoke(null, definition, context);
+                if (rule != null) {
+                    if (logger.isTraceEnabled()) {
+                        String msg = "Success parsing project rule definition \"{0}\" using {1}";
+                        logger.trace(msg, definition, method);
+                    }
+                    return rule;
+                } else if (logger.isTraceEnabled()) {
+                    String msg = "Unable to parse project rule definition \"{0}\" using {1}";
+                    logger.trace(msg, definition, method);
+                }
+            } catch (Throwable err) {
+                String msg = "Error while parsing project rule definition \"{0}\" using {1}";
+                logger.trace(err, msg, definition, method);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Parse string forms of an arry of rule definitions and return the rules
+     * 
+     * @param context the environment in which this method is being executed; may not be null
+     * @param definitions the definition of the rules that are to be parsed
+     * @return the rule, or null if the definition could not be parsed
+     */
+    public Rule[] rulesFromStrings( ExecutionContext context,
+                                    String... definitions ) {
+        List<Rule> rules = new LinkedList<Rule>();
+        for (String definition : definitions) {
+            Rule rule = ruleFromString(definition, context);
+            if (rule != null) rules.add(rule);
+        }
+        return rules.toArray(new Rule[rules.size()]);
+    }
+
+    /**
+     * Parse a single string containing one or more string forms of rule definitions, and return the rules. The string contains
+     * each rule on a separate line.
+     * 
+     * @param context the environment in which this method is being executed; may not be null
+     * @param definitions the definitions of the rules that are to be parsed, each definition separated by a newline character.
+     * @return the rule, or null if the definition could not be parsed
+     */
+    public Rule[] rulesFromString( ExecutionContext context,
+                                   String definitions ) {
+        List<String> lines = StringUtil.splitLines(definitions);
+        List<Rule> rules = new LinkedList<Rule>();
+        for (String definition : lines) {
+            Rule rule = ruleFromString(definition, context);
+            if (rule != null) rules.add(rule);
+        }
+        return rules.toArray(new Rule[rules.size()]);
+    }
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectionParser.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projector.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projector.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projector.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,50 @@
+/*
+ * 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.connector.federation;
+
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+
+/**
+ * Interface for the components that compute the {@link ProjectedNode} for a given location. Implementations may be provided for
+ * different configurations of {@link FederatedWorkspace#getProjections() projections}.
+ */
+ at Immutable
+interface Projector {
+
+    /**
+     * Project the supplied location in the federated repository into the equivalent projected node(s).
+     * 
+     * @param context the execution context in which the content is being accessed; may not be null
+     * @param location the location in the federated repository; may not be null
+     * @param requiresUpdate true if the operation for which this projection is needed will update the content in some way, or
+     *        false if read-only operations will be performed
+     * @return the projected node, or null if the node does not exist in any projection or if the operation requires update and no
+     *         writable projection applies
+     */
+    ProjectedNode project( ExecutionContext context,
+                           Location location,
+                           boolean requiresUpdate );
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/Projector.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholders.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholders.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholders.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,126 @@
+/*
+ * 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.connector.federation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import net.jcip.annotations.Immutable;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+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 com.google.common.collect.Multimaps;
+import com.google.common.collect.TreeMultimap;
+
+/**
+ * A Projector for federated repository configurations that are an offset, direct one-for-one mirror against a single source
+ * repository that is projected below the federated root. In other words, the federated repository has a single projection with a
+ * single "/something/below/root => /" rule.
+ */
+ at Immutable
+abstract class ProjectorWithPlaceholders implements Projector {
+
+    private Map<Path, PlaceholderNode> placeholderNodesByPath;
+    private Map<UUID, PlaceholderNode> placeholderNodesByUuid;
+
+    ProjectorWithPlaceholders( ExecutionContext context,
+                               List<Projection> projections ) {
+        placeholderNodesByPath = new HashMap<Path, PlaceholderNode>();
+        placeholderNodesByUuid = new HashMap<UUID, PlaceholderNode>();
+        // Use the execution context of the source to load the projected nodes ...
+        Collection<PlaceholderNode> placeholders = new ArrayList<PlaceholderNode>();
+        loadPlaceholderNodes(context, projections, placeholders);
+        for (PlaceholderNode placeholder : placeholders) {
+            placeholderNodesByPath.put(placeholder.location().getPath(), placeholder);
+            placeholderNodesByUuid.put(placeholder.location().getUuid(), placeholder);
+        }
+    }
+
+    /**
+     * Determine whether the specified location is a placeholder node.
+     * 
+     * @param location the location of the node; may not be null
+     * @return the placeholder node, or null if the supplied location does not designate a placeholder node
+     */
+    public PlaceholderNode isPlaceholder( Location location ) {
+        Path path = location.getPath();
+        if (path != null) {
+            return placeholderNodesByPath.get(path);
+        }
+        UUID uuid = location.getUuid();
+        if (uuid != null) {
+            return placeholderNodesByUuid.get(uuid);
+        }
+        return null;
+    }
+
+    /**
+     * Load the placeholder nodes for this repository. This method does not modify any state of this instance, but instead
+     * populates the supplied map.
+     * <p>
+     * A placeholder is created for each node above the projections' {@link Projection#getTopLevelPathsInRepository(PathFactory)
+     * top-level paths} in the federated repository. Thus, the projected node immediately above a top-level path will contain the
+     * top-level path as a child.
+     * </p>
+     * 
+     * @param context the context in which the placeholder nodes should be materialized; may not be null
+     * @param projections the projections; may not be null
+     * @param placeholderNodes the collection into which should be placed the materialized placeholders
+     */
+    protected static void loadPlaceholderNodes( ExecutionContext context,
+                                                Iterable<Projection> projections,
+                                                Collection<PlaceholderNode> placeholderNodes ) {
+        final PathFactory pathFactory = context.getValueFactories().getPathFactory();
+        TreeMultimap<Path, Location> childrenForParent = Multimaps.newTreeMultimap();
+        for (Projection projection : projections) {
+            // Collect the paths to all of the top-level nodes under each of their parents ...
+            for (Path path : projection.getTopLevelPathsInRepository(pathFactory)) {
+                if (path.isRoot()) continue;
+                while (!path.isRoot()) {
+                    // Create a projected node for the parent of this path ...
+                    Path parent = path.getParent();
+                    childrenForParent.put(parent, Location.create(path));
+                    path = parent;
+                }
+            }
+        }
+        // Now, we want to create a placeholder for each parent, with the list of all children ...
+        for (Path parentPath : childrenForParent.keySet()) {
+            // Get the children for this parent ...
+            List<Location> children = new ArrayList<Location>(childrenForParent.get(parentPath));
+            Map<Name, Property> properties = Collections.emptyMap();
+            Location location = Location.create(parentPath, UUID.randomUUID());
+            PlaceholderNode placeholder = new PlaceholderNode(location, properties, children);
+            placeholderNodes.add(placeholder);
+        }
+    }
+
+}


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholders.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/package-info.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/package-info.java	                        (rev 0)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/package-info.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,306 @@
+/*
+ * 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.
+ */
+/**
+ * JBoss DNA provides a federated connector that is able to access repository content from multiple external systems
+ * and make that content look like it exists in a single unified repository.  Like other connectors, a 
+ * {@link org.jboss.dna.graph.connector.RepositorySource} implementation is provided, called {@link FederatedRepositorySource},
+ * that can be set up and configured to be used by a {@link org.jboss.dna.graph.Graph} or a JCR repository. 
+ * 
+ * <h3>Projections</h3>
+ * <p>
+ * Each federated repository source provides a unified repository consisting of information that is dynamically federated
+ * from multiple other RepositorySource instances. The connector is configured with a number of <strong>projections</strong>
+ * that each describe where in the unified repository the federated connector should place the content from another source.
+ * Projections consist of the name of the source containing the content and a number of <strong>rules</strong> that
+ * define the path mappings, where each rule is defined as a string with this format:
+ * </p>
+ * <pre>
+ *       pathInFederatedRepository => pathInSourceRepository
+ * </pre>
+ * <p>
+ * Here, the <code>pathInFederatedRepository</code> is the string representation of the path in the unified
+ * (or federated) repository, and <code>pathInSourceRepository</code> is the string representation of the path of the
+ * actual content in the underlying source.  For example:
+ * </p>
+ * <pre>
+ *       / => /
+ * </pre>
+ * <p>
+ * is a trivial rule that states that all of the content in the underlying source should be mapped into the unified
+ * repository such that the locations are the same.  Therefore, a node at <code>/a/b/c</code> in the source would
+ * appear in the unified repository at <code>/a/b/c</code>.  This is called a <strong>mirror projection</strong>,
+ * since the unified repository mirrors the underlying source repository.
+ * </p>
+ * <p>
+ * Another example is an <strong>offset projection</strong>, which is similar to the mirror projection except that
+ * the federated path includes an offset not found in the source:
+ * </p>
+ * <pre>
+ *       /alpha/beta => /
+ * </pre>
+ * <p>
+ * Here, a node at <code>/a/b/c</code> in the source would actually appear in the unified repository at 
+ * <code>/alpha/beta/a/b/c</code>.  The offset path (<code>/alpha/beta</code> in this example) can have 1 or more segments.
+ * (If there are no segments, then it reduces to a mirror projection.)
+ * </p>
+ * <p>
+ * Often a rule will map a path in one source into another path in the unified source:
+ * </p>
+ * <pre>
+ *       /alpha/beta => /foo/bar
+ * </pre>
+ * <p>
+ * Here, the content at <code>/foo/bar</code> is projected in the unified repository under <code>/alpha/beta</code>,
+ * meaning that the <code>/foo/bar</code> prefix never even appears in the unified repository.  So the node at
+ * <code>/foo/bar/baz/raz</code> would appear in the unified repository at <code>/alpha/beta/baz/raz</code>.  Again,
+ * the size of the two paths in the rule don't matter.
+ * </p>
+ * <h3>Multiple Projections</h3>
+ * <p>
+ * Federated repositories that use a single projection are useful, but they aren't as interesting or powerful as
+ * those that use multiple projections.  Consider a federated repository that is defined by two projections:
+ * </p>
+ * <pre>
+ *       / => /                         for source "S1"
+ *       /alpha => /foo/bar             for source "S2"
+ * </pre>
+ * <p>
+ * And consider that S1 contains the following structure:
+ * </p>
+ * <pre>
+ *      +- a
+ *      |  +- i
+ *      |  +- j
+ *      +- b
+ *         +- k
+ *         +- m
+ *         +- n
+ * </pre>
+ * and S2 contains the following:
+ * <pre>
+ *      +- foo
+ *         +- bar
+ *         |  +- baz
+ *         |  |  +- taz
+ *         |  |  +- zaz
+ *         |  +- raz
+ *         +- bum
+ *            +- bot
+ * </pre>
+ * <p>
+ * The unified repository would then have this structure:
+ * </p>
+ * <pre>
+ *      +- a
+ *      |  +- i
+ *      |  +- j
+ *      +- b
+ *      |  +- k
+ *      |  +- m
+ *      |  +- n
+ *      +- alpha
+ *         +- baz
+ *            +- taz
+ *            |  +- zaz
+ *            +- raz
+ * </pre>
+ * <p>
+ * Note how the <code>/foo/bum</code> branch does not even appear in the unified repository, since it is outside of the
+ * branch being projected. Also, the <code>/alpha</code> node doesn't exist in S1 or S2; it's what is called a
+ * <strong>placeholder</strong> node that exists purely so that the nodes below it have a place to exist.
+ * Placeholders are somewhat special: they allow any structure below them (including other placeholder nodes or real
+ * projected nodes), but they cannot be modified.
+ * </p>
+ * <p>
+ * Even more interesting are cases that involve more projections.  Consider a federated repository that contains
+ * information about different kinds of automobiles, aircraft, and spacecraft, except that the information
+ * about each kind of vehicle exists in a different source (and possibly a different <i>kind</i> of source, such as
+ * a database, or file, or web service).
+ * <p>
+ * First, the sources.  The "Cars" source contains the following structure:
+ * </p>
+ * <pre>
+ *      +- Cars
+ *         +- Hybrid
+ *         |  +- Toyota Prius
+ *         |  +- Toyota Highlander
+ *         |  +- Nissan Altima
+ *         +- Sports
+ *         |  +- Aston Martin DB9
+ *         |  +- Infinity G37
+ *         +- Luxury
+ *         |  +- Cadillac DTS
+ *         |  +- Bentley Continental
+ *         |  +- Lexus IS350
+ *         +- Utility
+ *            +- Land Rover LR2
+ *            +- Land Rover LR3
+ *            +- Hummer H3
+ *            +- Ford F-150
+ * </pre>
+ * <p>
+ * The "Aircraft" source contains the following structure:
+ * </p>
+ * <pre>
+ *      +- Aviation
+ *         +- Business
+ *         |  +- Gulfstream V
+ *         |  +- Learjet 45
+ *         +- Commercial
+ *         |  +- Boeing 777
+ *         |  +- Boeing 767
+ *         |  +- Boeing 787
+ *         |  +- Boeing 757
+ *         |  +- Airbus A380
+ *         |  +- Airbus A340
+ *         |  +- Airbus A310
+ *         |  +- Embraer RJ-175
+ *         +- Vintage
+ *         |  +- Fokker Trimotor
+ *         |  +- P-38 Lightning
+ *         |  +- A6M Zero
+ *         |  +- Bf 109
+ *         |  +- Wright Flyer
+ *         +- Homebuilt
+ *            +- Long-EZ
+ *            +- Cirrus VK-30
+ *            +- Van's RV-4
+ * </pre>
+ * <p>
+ * Finally, our "Spacecraft" source contains the following structure:
+ * </p>
+ * <pre>
+ *      +- Space Vehicles
+ *         +- Manned
+ *         |  +- Space Shuttle
+ *         |  +- Soyuz
+ *         |  +- Skylab
+ *         |  +- ISS
+ *         +- Unmanned
+ *         |  +- Sputnik
+ *         |  +- Explorer
+ *         |  +- Vanguard
+ *         |  +- Pioneer
+ *         |  +- Marsnik
+ *         |  +- Mariner
+ *         |  +- Mars Pathfinder
+ *         |  +- Mars Observer
+ *         |  +- Mars Polar Lander
+ *         +- Launch Vehicles
+ *         |  +- Saturn V
+ *         |  +- Aries
+ *         |  +- Delta
+ *         |  +- Delta II
+ *         |  +- Orion
+ *         +- X-Prize
+ *            +- SpaceShipOne
+ *            +- WildFire
+ *            +- Spirit of Liberty
+ * </pre>
+ * <p>
+ * So, we can define our unified "Vehicles" source with the following projections:
+ * </p>
+ * <pre>
+ *       /Vehicles => /                                  for source "Cars"
+ *       /Vehicles/Aircraft => /Aviation                 for source "Aircraft"
+ *       /Vehicles/Spacecraft => /Space Vehicles         for source "Cars"
+ * </pre>
+ * <p>
+ * The result is a unified repository with the following structure:
+ * </p>
+ * <pre>
+ *      +- Vehicles
+ *         +- Cars
+ *         |  +- Hybrid
+ *         |   |  +- Toyota Prius
+ *         |   |  +- Toyota Highlander
+ *         |   |  +- Nissan Altima
+ *         |   +- Sports
+ *         |   |  +- Aston Martin DB9
+ *         |   |  +- Infinity G37
+ *         |   +- Luxury
+ *         |   |  +- Cadillac DTS
+ *         |   |  +- Bentley Continental
+ *         |  +- Lexus IS350
+ *         |  +- Utility
+ *         |     +- Land Rover LR2
+ *         |     +- Land Rover LR3
+ *         |     +- Hummer H3
+ *         |     +- Ford F-150
+ *         +- Aircraft
+ *         |   +- Business
+ *         |   |  +- Gulfstream V
+ *         |   |  +- Learjet 45
+ *         |   +- Commercial
+ *         |   |  +- Boeing 777
+ *         |   |  +- Boeing 767
+ *         |   |  +- Boeing 787
+ *         |   |  +- Boeing 757
+ *         |   |  +- Airbus A380
+ *         |   |  +- Airbus A340
+ *         |   |  +- Airbus A310
+ *         |   |  +- Embraer RJ-175
+ *         |   +- Vintage
+ *         |   |  +- Fokker Trimotor
+ *         |   |  +- P-38 Lightning
+ *         |   |  +- A6M Zero
+ *         |   |  +- Bf 109
+ *         |   |  +- Wright Flyer
+ *         |   +- Homebuilt
+ *         |      +- Long-EZ
+ *         |      +- Cirrus VK-30
+ *         |      +- Van's RV-4
+ *         +- Spacecraft
+ *            +- Manned
+ *            |  +- Space Shuttle
+ *            |  +- Soyuz
+ *            |  +- Skylab
+ *            |  +- ISS
+ *            +- Unmanned
+ *            |  +- Sputnik
+ *            |  +- Explorer
+ *            |  +- Vanguard
+ *            |  +- Pioneer
+ *            |  +- Marsnik
+ *            |  +- Mariner
+ *            |  +- Mars Pathfinder
+ *            |  +- Mars Observer
+ *            |  +- Mars Polar Lander
+ *            +- Launch Vehicles
+ *            |  +- Saturn V
+ *            |  +- Aries
+ *            |  +- Delta
+ *            |  +- Delta II
+ *            |  +- Orion
+ *            +- X-Prize
+ *               +- SpaceShipOne
+ *               +- WildFire
+ *               +- Spirit of Liberty
+ * </pre>
+ * <p>
+ * Other combinations are of course possible.
+ * </p>
+ */
+package org.jboss.dna.graph.connector.federation;
+


Property changes on: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/package-info.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -157,7 +157,27 @@
                 break;
             }
         }
-        node = workspace.createNode(getExecutionContext(), parentNode, request.named(), uuid);
+        switch (request.conflictBehavior()) {
+            case APPEND:
+            case DO_NOT_REPLACE:
+                node = workspace.createNode(getExecutionContext(), parentNode, request.named(), uuid);
+                break;
+            case REPLACE:
+                // See if the node already exists (this doesn't record an error on the request) ...
+                node = getTargetNode(workspace, null, Location.create(pathFactory.create(parent, request.named()), uuid));
+                if (node != null) {
+                    workspace.removeNode(getExecutionContext(), node);
+                }
+                node = workspace.createNode(getExecutionContext(), parentNode, request.named(), uuid);
+                break;
+            case UPDATE:
+                // See if the node already exists (this doesn't record an error on the request) ...
+                node = getTargetNode(workspace, null, Location.create(pathFactory.create(parent, request.named()), uuid));
+                if (node == null) {
+                    node = workspace.createNode(getExecutionContext(), parentNode, request.named(), uuid);
+                } // otherwise, we found it and we're setting any properties below
+                break;
+        }
         assert node != null;
         Path path = getExecutionContext().getValueFactories().getPathFactory().create(parent, node.getName());
         // Now add the properties to the supplied node ...
@@ -381,7 +401,7 @@
                 node = workspace.getNode(path);
             }
         }
-        if (node == null) {
+        if (node == null && request != null) {
             if (path == null) {
                 if (uuid == null) {
                     // Missing both path and UUID ...

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CloneWorkspaceRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CloneWorkspaceRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CloneWorkspaceRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
 
@@ -178,11 +179,23 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocationOfRoot = null;
+        this.actualWorkspaceName = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#hashCode()
      */
     @Override
     public int hashCode() {
-        return desiredNameOfTargetWorkspace.hashCode();
+        return HashCode.compute(nameOfWorkspaceToBeCloned, desiredNameOfTargetWorkspace);
     }
 
     /**

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CompositeRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CompositeRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CompositeRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -46,6 +46,8 @@
 
     private static final long serialVersionUID = 1L;
 
+    public static final int UNKNOWN_NUMBER_OF_REQUESTS = Integer.MAX_VALUE;
+
     /**
      * Return a request that either wraps multiple requests, or the single request if only one is supplied.
      * 
@@ -195,8 +197,8 @@
      * @param requests the modifiable list of requests; may not be null
      * @param readOnly true if all of the requests are {@link Request#isReadOnly() read-only}
      */
-    /*package*/CompositeRequest( List<? extends Request> requests,
-                                  boolean readOnly ) {
+    protected CompositeRequest( List<? extends Request> requests,
+                                boolean readOnly ) {
         // Iterate through the requests and set the cancelled flag of each request to this object's flag ...
         final AtomicBoolean flag = super.getCancelledFlag();
         for (Request request : requests) {
@@ -207,6 +209,16 @@
     }
 
     /**
+     * Create a composite request from the supplied list of requests. This is useful only for subclasses.
+     * 
+     * @param readOnly true if all of the requests are {@link Request#isReadOnly() read-only}
+     */
+    protected CompositeRequest( boolean readOnly ) {
+        this.requests = Collections.emptyList();
+        this.readOnly = false;
+    }
+
+    /**
      * Return the unmodifiable requests contained in this composite request.
      * 
      * @return requests

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CopyBranchRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CopyBranchRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CopyBranchRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.NodeConflictBehavior;
@@ -267,6 +268,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(from, fromWorkspace, into, intoWorkspace);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualFromLocation = null;
+        this.actualIntoLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateNodeRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateNodeRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateNodeRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -30,6 +30,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.NodeConflictBehavior;
@@ -325,6 +326,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(under, childName, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateWorkspaceRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateWorkspaceRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/CreateWorkspaceRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -141,6 +141,18 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualWorkspaceName = null;
+        this.actualLocationOfRoot = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#hashCode()
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/DeleteBranchRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/DeleteBranchRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/DeleteBranchRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Path;
@@ -134,6 +135,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/GetWorkspacesRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/GetWorkspacesRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/GetWorkspacesRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -71,6 +71,17 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.availableWorkspaceNames = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#hashCode()
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.NodeConflictBehavior;
@@ -282,6 +283,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualOldLocation = null;
+        this.actualNewLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(from, workspaceName, into);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllChildrenRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllChildrenRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllChildrenRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -27,6 +27,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -110,6 +111,23 @@
     }
 
     /**
+     * Add to the list of children that has been read the supplied children with the given path and identification properties. The
+     * children are added in order.
+     * 
+     * @param children the locations of the children that were read
+     * @throws IllegalArgumentException if the parameter is null
+     * @see #addChild(Location)
+     * @see #addChild(Path, Property)
+     * @see #addChild(Path, Property, Property...)
+     */
+    public void addChildren( Iterable<Location> children ) {
+        CheckArg.isNotNull(children, "children");
+        for (Location child : children) {
+            if (child != null) this.children.add(child);
+        }
+    }
+
+    /**
      * Add to the list of children that has been read the child with the given path and identification properties. The children
      * should be added in order.
      * 
@@ -190,6 +208,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualOf = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(of, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllPropertiesRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllPropertiesRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadAllPropertiesRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -28,6 +28,7 @@
 import java.util.Iterator;
 import java.util.Map;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -146,6 +147,18 @@
     }
 
     /**
+     * Add a property that was read from the {@link RepositoryConnection}
+     * 
+     * @param properties the properties that were read
+     * @throws IllegalArgumentException if the property is null
+     */
+    public void addProperties( Iterable<Property> properties ) {
+        for (Property property : properties) {
+            this.properties.put(property.getName(), property);
+        }
+    }
+
+    /**
      * Get the number of children for this node.
      * 
      * @return the number of children, or {@link #UNKNOWN_NUMBER_OF_CHILDREN} if the number of children was not yet read
@@ -196,6 +209,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBlockOfChildrenRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBlockOfChildrenRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBlockOfChildrenRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -27,6 +27,7 @@
 import java.util.List;
 import org.jboss.dna.common.text.Inflector;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -157,6 +158,23 @@
     }
 
     /**
+     * Add to the list of children that has been read the supplied children with the given path and identification properties. The
+     * children are added in order.
+     * 
+     * @param children the locations of the children that were read
+     * @throws IllegalArgumentException if the parameter is null
+     * @see #addChild(Location)
+     * @see #addChild(Path, Property)
+     * @see #addChild(Path, Property, Property...)
+     */
+    public void addChildren( Iterable<Location> children ) {
+        CheckArg.isNotNull(children, "children");
+        for (Location child : children) {
+            if (child != null) this.children.add(child);
+        }
+    }
+
+    /**
      * Add to the list of children that has been read the child with the given path and identification properties. The children
      * should be added in order.
      * 
@@ -235,6 +253,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+        this.children.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(of, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBranchRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBranchRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadBranchRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -32,6 +32,7 @@
 import java.util.NoSuchElementException;
 import net.jcip.annotations.NotThreadSafe;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -382,6 +383,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+        this.nodes.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNextBlockOfChildrenRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNextBlockOfChildrenRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNextBlockOfChildrenRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -26,6 +26,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -125,6 +126,23 @@
     }
 
     /**
+     * Add to the list of children that has been read the supplied children with the given path and identification properties. The
+     * children are added in order.
+     * 
+     * @param children the locations of the children that were read
+     * @throws IllegalArgumentException if the parameter is null
+     * @see #addChild(Location)
+     * @see #addChild(Path, Property)
+     * @see #addChild(Path, Property, Property...)
+     */
+    public void addChildren( Iterable<Location> children ) {
+        CheckArg.isNotNull(children, "children");
+        for (Location child : children) {
+            if (child != null) this.children.add(child);
+        }
+    }
+
+    /**
      * Add to the list of children that has been read the child with the given path and identification properties. The children
      * should be added in order.
      * 
@@ -204,6 +222,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualStartingAfter = null;
+        this.children.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(startingAfter, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNodeRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNodeRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadNodeRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -30,6 +30,7 @@
 import java.util.List;
 import java.util.Map;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -138,6 +139,18 @@
     }
 
     /**
+     * Add a property that was read from the {@link RepositoryConnection}
+     * 
+     * @param properties the properties that were read
+     * @throws IllegalArgumentException if the property is null
+     */
+    public void addProperties( Iterable<Property> properties ) {
+        for (Property property : properties) {
+            this.properties.put(property.getName(), property);
+        }
+    }
+
+    /**
      * Get the children that were read from the {@link RepositoryConnection} after the request was processed. Each child is
      * represented by a location.
      * 
@@ -157,6 +170,23 @@
     }
 
     /**
+     * Add to the list of children that has been read the supplied children with the given path and identification properties. The
+     * children are added in order.
+     * 
+     * @param children the locations of the children that were read
+     * @throws IllegalArgumentException if the parameter is null
+     * @see #addChild(Location)
+     * @see #addChild(Path, Property)
+     * @see #addChild(Path, Property, Property...)
+     */
+    public void addChildren( Iterable<Location> children ) {
+        CheckArg.isNotNull(children, "children");
+        for (Location child : children) {
+            if (child != null) this.children.add(child);
+        }
+    }
+
+    /**
      * Add to the list of children that has been read the child with the given path and identification properties. The children
      * should be added in order.
      * 
@@ -235,6 +265,29 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+        this.children.clear();
+        this.properties.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadPropertyRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadPropertyRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/ReadPropertyRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -153,6 +154,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+        this.property = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(on, workspaceName, propertyName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RemovePropertyRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RemovePropertyRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RemovePropertyRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Name;
@@ -141,6 +142,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(from, workspaceName, propertyName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RenameNodeRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RenameNodeRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/RenameNodeRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Name;
@@ -180,6 +181,28 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualNewLocation = null;
+        this.actualOldLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/SetPropertyRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/SetPropertyRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/SetPropertyRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Path;
@@ -141,6 +142,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(on, workspaceName, property.getName());
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/UpdatePropertiesRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/UpdatePropertiesRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/UpdatePropertiesRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -26,6 +26,7 @@
 import java.util.Collections;
 import java.util.Map;
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 import org.jboss.dna.graph.property.Name;
@@ -159,6 +160,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(on, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyNodeExistsRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyNodeExistsRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyNodeExistsRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -24,6 +24,7 @@
 package org.jboss.dna.graph.request;
 
 import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.graph.GraphI18n;
 import org.jboss.dna.graph.Location;
 
@@ -126,6 +127,27 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocation = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(at, workspaceName);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyWorkspaceRequest.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyWorkspaceRequest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/VerifyWorkspaceRequest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -106,6 +106,18 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.jboss.dna.graph.request.Request#cancel()
+     */
+    @Override
+    public void cancel() {
+        super.cancel();
+        this.actualLocationOfRoot = null;
+        this.actualWorkspaceName = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see java.lang.Object#hashCode()
      */
     @Override

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -111,7 +111,7 @@
      * 
      * @return the repository source name; never null or empty
      */
-    public String getSourceName() {
+    public final String getSourceName() {
         return sourceName;
     }
 
@@ -120,7 +120,7 @@
      * 
      * @return the execution context; never null
      */
-    public ExecutionContext getExecutionContext() {
+    public final ExecutionContext getExecutionContext() {
         return this.context;
     }
 
@@ -129,17 +129,27 @@
      * 
      * @return the current time in UTC; never null
      */
-    protected DateTime getNowInUtc() {
+    protected final DateTime getNowInUtc() {
         return this.nowInUtc;
     }
 
     /**
+     * @return defaultCachePolicy
+     */
+    protected final CachePolicy getDefaultCachePolicy() {
+        return defaultCachePolicy;
+    }
+
+    /**
      * Set the supplied request to have the default cache policy and the {@link #getNowInUtc() current time in UTC}.
      * 
      * @param request the cacheable request
      */
     protected void setCacheableInfo( CacheableRequest request ) {
-        request.setCachePolicy(defaultCachePolicy);
+        // Set it only if the request has no cache policy already ...
+        if (request.getCachePolicy() == null && defaultCachePolicy != null) {
+            request.setCachePolicy(defaultCachePolicy);
+        }
         request.setTimeLoaded(nowInUtc);
     }
 
@@ -151,7 +161,18 @@
      */
     protected void setCacheableInfo( CacheableRequest request,
                                      CachePolicy cachePolicy ) {
-        request.setCachePolicy(cachePolicy);
+        if (cachePolicy == null) cachePolicy = defaultCachePolicy;
+        if (cachePolicy != null) {
+            if (request.getCachePolicy() != null) {
+                // Set the supplied only if less than the current ...
+                if (request.getCachePolicy().getTimeToLive() > cachePolicy.getTimeToLive()) {
+                    request.setCachePolicy(cachePolicy);
+                }
+            } else {
+                // There is no current policy, so set the supplied policy ...
+                request.setCachePolicy(cachePolicy);
+            }
+        }
         request.setTimeLoaded(nowInUtc);
     }
 
@@ -175,6 +196,8 @@
             process((CreateNodeRequest)request);
         } else if (request instanceof DeleteBranchRequest) {
             process((DeleteBranchRequest)request);
+        } else if (request instanceof DeleteChildrenRequest) {
+            process((DeleteChildrenRequest)request);
         } else if (request instanceof MoveBranchRequest) {
             process((MoveBranchRequest)request);
         } else if (request instanceof ReadAllChildrenRequest) {
@@ -229,6 +252,7 @@
         if (request == null) return;
         int numberOfErrors = 0;
         List<Throwable> errors = null;
+        // Iterate over the requests in this composite, but only iterate once so that
         for (Request embedded : request) {
             assert embedded != null;
             if (embedded.isCancelled()) return;
@@ -252,9 +276,12 @@
                 errorString.append("\n");
                 errorString.append("\t" + error.getMessage());
             }
-            String msg = GraphI18n.multipleErrorsWhileExecutingRequests.text(numberOfErrors,
-                                                                             request.size(),
-                                                                             errorString.toString());
+            String msg = null;
+            if (request.size() == CompositeRequest.UNKNOWN_NUMBER_OF_REQUESTS) {
+                msg = GraphI18n.multipleErrorsWhileExecutingManyRequests.text(numberOfErrors, errorString.toString());
+            } else {
+                msg = GraphI18n.multipleErrorsWhileExecutingRequests.text(numberOfErrors, request.size(), errorString.toString());
+            }
             request.setError(new RepositorySourceException(getSourceName(), msg));
         }
     }

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-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/main/resources/org/jboss/dna/graph/GraphI18n.properties	2009-05-18 15:36:58 UTC (rev 911)
@@ -55,6 +55,7 @@
 executedRequest = Executed {0}
 closingRequestProcessor = Closing request processor
 closedRequestProcessor = Closed request processor
+multipleErrorsWhileExecutingManyRequests = {0} of the many requests resulted in errors: {1}
 multipleErrorsWhileExecutingRequests = {0} of the {1} requests resulted in errors: {2}
 unsupportedRequestType = Requests of type "{0}" are unsupported; actual request was to {1}
 unableToAddMoreRequestsToAlreadyExecutedBatch = Unable to add more requests to a batch of graph requests that has already been executed
@@ -75,9 +76,29 @@
 unableToFindRepositorySourceWithName = Unable to find a repository source named "{0}"
 
 # In-memory connector
-inMemoryConnectorName = In-Memory Connector
 inMemoryNodeDoesNotExist = Could not find an existing node at {0}
 errorSerializingInMemoryCachePolicyInSource = Error serializing a {0} instance owned by the {1} in-memory repository
 inMemoryConnectorRequestsMustHavePathOrUuid = In-Memory connector can only process requests with a path and/or UUID
 workspaceDoesNotExistInRepository = The workspace "{0}" does not exist in the "{1}" in-memory repository
 workspaceAlreadyExistsInRepository = The workspace "{0}" already exists in the "{1}" in-memory repository
+
+# Federation connector
+namePropertyIsRequiredForFederatedRepositorySource = The "{0}" property is required on each federated repository source
+propertyIsRequiredForFederatedRepositorySource = The "{0}" property is required on the "{1}" federated repository source
+federatedRepositorySourceMustBeInitialized = The "{0}" federated repository source must be initialized before it can be used
+errorReadingConfigurationForFederatedRepositorySource = Error with the "{0}" federated repository source's configuration at "{3}" in the "{2}" workspace of "{1}"
+errorAddingProjectionRuleParseMethod = Error while adding a parsing method for a federation projection rule
+requiredNodeDoesNotExistRelativeToNode = The required node "{0}" does not exist relative to "{1}" in "{2}" workspace of the "{3}" repository
+unableToObtainConnectionToFederatedSource = Unable to obtain a connection to source "{0}" being federated by the "{1}" workspace of the "{2}" federated repository
+workspaceDoesNotExistInFederatedRepository = The workspace "{0}" does not exist in the "{1}" federated repository
+locationCannotBeProjectedIntoWorkspaceAndSource = {0} does not exist in the "{1}" workspace of the "{2}" federated repository
+unableToAddRequestToChannelThatIsDone = Unable to add a request to the "{0}" channel since it is done: {1}
+federatedSourceDoesNotSupportCreatingWorkspaces = {0} is a source that does not allow creating workspaces
+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}" 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

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryConnection.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryConnection.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryConnection.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,146 @@
+/*
+ * 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.connector;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+import javax.transaction.xa.XAResource;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * A simple connection to a mock repository, where the connection accepts requests but does very little with them (other than
+ * record that requests were processed in the Queue supplied in the constructor).
+ */
+public class MockRepositoryConnection implements RepositoryConnection {
+
+    private final String sourceName;
+    private final Queue<Request> processed;
+
+    /**
+     * Create a new connection
+     * 
+     * @param sourceName the name of the source that this connection represents; may not be null
+     */
+    public MockRepositoryConnection( String sourceName ) {
+        this(sourceName, new LinkedList<Request>());
+    }
+
+    /**
+     * Create a new connection
+     * 
+     * @param sourceName the name of the source that this connection represents; may not be null
+     * @param processed a queue into which should be placed all requests that were {@link #execute(ExecutionContext, Request)
+     *        executed}; may be null
+     */
+    public MockRepositoryConnection( String sourceName,
+                                     Queue<Request> processed ) {
+        assert sourceName != null;
+        this.sourceName = sourceName;
+        this.processed = processed != null ? processed : new LinkedList<Request>();
+    }
+
+    /**
+     * Get the list of requests that have been processed.
+     * 
+     * @return the queue of processed requests; never null
+     */
+    public Queue<Request> getProcessedRequests() {
+        return processed;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#close()
+     */
+    public void close() {
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#execute(org.jboss.dna.graph.ExecutionContext,
+     *      org.jboss.dna.graph.request.Request)
+     */
+    public void execute( ExecutionContext context,
+                         Request request ) throws RepositorySourceException {
+        RequestProcessor processor = new MockRepositoryRequestProcessor(sourceName, context, processed);
+        try {
+            processor.process(request);
+        } finally {
+            processor.close();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getDefaultCachePolicy()
+     */
+    public CachePolicy getDefaultCachePolicy() {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getSourceName()
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#getXAResource()
+     */
+    public XAResource getXAResource() {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#ping(long, java.util.concurrent.TimeUnit)
+     */
+    public boolean ping( long time,
+                         TimeUnit unit ) {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.connector.RepositoryConnection#setListener(org.jboss.dna.graph.connector.RepositorySourceListener)
+     */
+    public void setListener( RepositorySourceListener listener ) {
+        throw new UnsupportedOperationException();
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryConnection.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryRequestProcessor.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryRequestProcessor.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryRequestProcessor.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,296 @@
+/*
+ * 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.connector;
+
+import java.util.Queue;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.request.CloneWorkspaceRequest;
+import org.jboss.dna.graph.request.CopyBranchRequest;
+import org.jboss.dna.graph.request.CreateNodeRequest;
+import org.jboss.dna.graph.request.CreateWorkspaceRequest;
+import org.jboss.dna.graph.request.DeleteBranchRequest;
+import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
+import org.jboss.dna.graph.request.GetWorkspacesRequest;
+import org.jboss.dna.graph.request.MoveBranchRequest;
+import org.jboss.dna.graph.request.ReadAllChildrenRequest;
+import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
+import org.jboss.dna.graph.request.ReadBlockOfChildrenRequest;
+import org.jboss.dna.graph.request.ReadBranchRequest;
+import org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.jboss.dna.graph.request.ReadPropertyRequest;
+import org.jboss.dna.graph.request.RemovePropertyRequest;
+import org.jboss.dna.graph.request.RenameNodeRequest;
+import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.SetPropertyRequest;
+import org.jboss.dna.graph.request.UpdatePropertiesRequest;
+import org.jboss.dna.graph.request.VerifyNodeExistsRequest;
+import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
+import org.jboss.dna.graph.request.processor.RequestProcessor;
+
+/**
+ * 
+ */
+public class MockRepositoryRequestProcessor extends RequestProcessor {
+
+    private Queue<Request> processed;
+
+    /**
+     * @param sourceName
+     * @param context
+     * @param processed
+     */
+    public MockRepositoryRequestProcessor( String sourceName,
+                                           ExecutionContext context,
+                                           Queue<Request> processed ) {
+        super(sourceName, context);
+        assert processed != null;
+        this.processed = processed;
+    }
+
+    protected void record( Request request ) {
+        processed.add(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
+     */
+    @Override
+    public void process( VerifyWorkspaceRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
+     */
+    @Override
+    public void process( GetWorkspacesRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
+     */
+    @Override
+    public void process( CreateWorkspaceRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
+     */
+    @Override
+    public void process( CloneWorkspaceRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
+     */
+    @Override
+    public void process( DestroyWorkspaceRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
+     */
+    @Override
+    public void process( CopyBranchRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
+     */
+    @Override
+    public void process( CreateNodeRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
+     */
+    @Override
+    public void process( DeleteBranchRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
+     */
+    @Override
+    public void process( MoveBranchRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
+     */
+    @Override
+    public void process( ReadAllChildrenRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
+     */
+    @Override
+    public void process( ReadAllPropertiesRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
+     */
+    @Override
+    public void process( UpdatePropertiesRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBlockOfChildrenRequest)
+     */
+    @Override
+    public void process( ReadBlockOfChildrenRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBranchRequest)
+     */
+    @Override
+    public void process( ReadBranchRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest)
+     */
+    @Override
+    public void process( ReadNextBlockOfChildrenRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNodeRequest)
+     */
+    @Override
+    public void process( ReadNodeRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadPropertyRequest)
+     */
+    @Override
+    public void process( ReadPropertyRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.RemovePropertyRequest)
+     */
+    @Override
+    public void process( RemovePropertyRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.RenameNodeRequest)
+     */
+    @Override
+    public void process( RenameNodeRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.SetPropertyRequest)
+     */
+    @Override
+    public void process( SetPropertyRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyNodeExistsRequest)
+     */
+    @Override
+    public void process( VerifyNodeExistsRequest request ) {
+        record(request);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.request.processor.RequestProcessor#processUnknownRequest(org.jboss.dna.graph.request.Request)
+     */
+    @Override
+    protected void processUnknownRequest( Request request ) {
+        record(request);
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/MockRepositoryRequestProcessor.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,319 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.jboss.dna.common.statistic.Stopwatch;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Node;
+import org.jboss.dna.graph.Subgraph;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.RepositoryContext;
+import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.connector.test.AbstractConnectorTest;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathNotFoundException;
+import org.jboss.dna.graph.property.Property;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * 
+ */
+public abstract class AbstractFederatedRepositorySourceIntegrationTest {
+
+    private static final Stopwatch FEDERATED_TIMER = new Stopwatch();
+    private static final Stopwatch SOURCE_TIMER = new Stopwatch();
+
+    protected FederatedRepositorySource source;
+    private String sourceName;
+    private String repositoryName;
+    private String configurationSourceName;
+    private String configurationWorkspaceName;
+    private InMemoryRepositorySource configRepositorySource;
+    private RepositoryConnection configRepositoryConnection;
+    protected ExecutionContext context;
+    private Map<String, InMemoryRepositorySource> sources;
+    protected Graph federated;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+    @Mock
+    private RepositoryContext repositoryContext;
+
+    /**
+     * @throws java.lang.Exception
+     */
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        context = new ExecutionContext();
+        configurationSourceName = "configuration";
+        configurationWorkspaceName = "configSpace";
+        repositoryName = "Test Repository";
+
+        // Set up the source ...
+        source = new FederatedRepositorySource();
+        source.setName(repositoryName);
+        sourceName = "federated source";
+        source.setName(sourceName);
+        source.setConfigurationSourceName(configurationSourceName);
+        source.setConfigurationWorkspaceName(configurationWorkspaceName);
+        source.setConfigurationPath("/a/b/Test Repository");
+        source.initialize(repositoryContext);
+
+        // Set up the configuration repository ...
+        configRepositorySource = new InMemoryRepositorySource();
+        configRepositorySource.setName("Configuration Repository");
+        configRepositorySource.setDefaultWorkspaceName(configurationWorkspaceName);
+        Graph config = Graph.create(configRepositorySource, context);
+        config.create("/a");
+        config.create("/a/b");
+        config.create("/a/b/Test Repository");
+        config.create("/a/b/Test Repository/dna:workspaces");
+
+        // Set up the map of sources ...
+        sources = new HashMap<String, InMemoryRepositorySource>();
+
+        // Stub the RepositoryContext and RepositoryConnectionFactory instances ...
+        configRepositoryConnection = configRepositorySource.getConnection();
+        stub(repositoryContext.getExecutionContext()).toReturn(context);
+        stub(repositoryContext.getRepositoryConnectionFactory()).toReturn(connectionFactory);
+        stub(connectionFactory.createConnection(configurationSourceName)).toReturn(configRepositoryConnection);
+        stub(connectionFactory.createConnection(sourceName)).toAnswer(new Answer<RepositoryConnection>() {
+            public RepositoryConnection answer( InvocationOnMock invocation ) throws Throwable {
+                return source.getConnection();
+            }
+        });
+
+        // Create the graph to the federated repository ...
+        federated = Graph.create(sourceName, connectionFactory, context);
+    }
+
+    @AfterClass
+    public static void afterAll() {
+        System.out.println("Results for federated reads:  " + FEDERATED_TIMER.getSimpleStatistics());
+        System.out.println("Results for source reads:     " + FEDERATED_TIMER.getSimpleStatistics());
+    }
+
+    /**
+     * Add to the supplied workspace in the federated repository a projection from the workspace in the supplied source.
+     * 
+     * @param federatedWorkspace
+     * @param projectionName
+     * @param sourceName
+     * @param workspaceName
+     * @param projectionRules
+     */
+    protected void addProjection( String federatedWorkspace,
+                                  String projectionName,
+                                  String sourceName,
+                                  String workspaceName,
+                                  String... projectionRules ) {
+        CheckArg.isNotNull(federatedWorkspace, "federatedWorkspace");
+        CheckArg.isNotNull(projectionName, "projectionName");
+        CheckArg.isNotNull(sourceName, "sourceName");
+        CheckArg.isNotNull(workspaceName, "workspaceName");
+        CheckArg.isNotEmpty(projectionRules, "projectionRules");
+        assertThat(source.getConfigurationPath().endsWith("/"), is(true));
+        String wsPath = source.getConfigurationPath() + "dna:workspaces/" + federatedWorkspace;
+        String projectionPath = wsPath + "/dna:projections/" + projectionName;
+        Graph config = Graph.create(configRepositorySource, context);
+        config.useWorkspace(configurationWorkspaceName);
+        config.createIfMissing(wsPath);
+        config.createIfMissing(wsPath + "/dna:projections");
+        config.createAt(projectionPath).with(FederatedLexicon.PROJECTION_RULES, (Object[])projectionRules).with(FederatedLexicon.SOURCE_NAME,
+                                                                                                                sourceName).with(FederatedLexicon.WORKSPACE_NAME,
+                                                                                                                                 workspaceName).and();
+        // Make sure the source and workspace exist ...
+        graphFor(sourceName, workspaceName);
+    }
+
+    /**
+     * Obtain a graph to the named source and workspace. If the source does not exist, it is created. Also, if the supplied
+     * workspace does not exist, it is also created.
+     * 
+     * @param sourceName the name of the source; may not be null
+     * @param workspaceName the name of the workspace; may not be null
+     * @return the resulting graph; never null
+     */
+    protected Graph graphFor( String sourceName,
+                              String workspaceName ) {
+        CheckArg.isNotNull(sourceName, "sourceName");
+        CheckArg.isNotNull(workspaceName, "workspaceName");
+        InMemoryRepositorySource source = sources.get(sourceName);
+        if (source == null) {
+            // Add a new source with this name ...
+            source = new InMemoryRepositorySource();
+            source.setName(sourceName);
+            sources.put(sourceName, source);
+            final InMemoryRepositorySource newSource = source;
+            // Stub the repository connection factory to return a new connection for this source ...
+            stub(connectionFactory.createConnection(sourceName)).toAnswer(new Answer<RepositoryConnection>() {
+                public RepositoryConnection answer( InvocationOnMock invocation ) throws Throwable {
+                    return newSource.getConnection();
+                }
+            });
+        }
+        // Make sure there's a workspace for it ...
+        Graph sourceGraph = Graph.create(sourceName, connectionFactory, context);
+        if (sourceGraph.getWorkspaces().contains(workspaceName)) {
+            sourceGraph.useWorkspace(workspaceName);
+        } else {
+            sourceGraph.createWorkspace().named(workspaceName);
+        }
+        return sourceGraph;
+    }
+
+    /**
+     * Assert that the node does not exist in the federated repository given by the supplied path nor in the underlying source
+     * given by the path, source name, and workspace name.
+     * 
+     * @param pathInFederated
+     * @param pathInSource
+     * @param sourceName
+     * @param workspaceName
+     */
+    protected void assertNoNode( String pathInFederated,
+                                 String pathInSource,
+                                 String sourceName,
+                                 String workspaceName ) {
+        try {
+            FEDERATED_TIMER.start();
+            federated.getNodeAt(pathInFederated);
+            FEDERATED_TIMER.stop();
+            fail("Did not expect to find federated node \"" + pathInFederated + "\"");
+        } catch (PathNotFoundException e) {
+            // expected
+        }
+        try {
+            SOURCE_TIMER.start();
+            graphFor(sourceName, workspaceName).getNodeAt(pathInSource);
+            SOURCE_TIMER.stop();
+            fail("Did not expect to find source node \"" + pathInSource + "\" in workspace \"" + workspaceName
+                 + "\" of source \"" + sourceName + "\"");
+        } catch (PathNotFoundException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Assert that the node in the federated repository given by the supplied path represents the same node in the underlying
+     * source given by the path, source name, and workspace name.
+     * 
+     * @param pathInFederated
+     * @param pathInSource
+     * @param sourceName
+     * @param workspaceName
+     * @param extraChildren
+     */
+    protected void assertSameNode( String pathInFederated,
+                                   String pathInSource,
+                                   String sourceName,
+                                   String workspaceName,
+                                   String... extraChildren ) {
+        FEDERATED_TIMER.start();
+        Node fedNode = federated.getNodeAt(pathInFederated);
+        FEDERATED_TIMER.stop();
+        SOURCE_TIMER.start();
+        Node sourceNode = graphFor(sourceName, workspaceName).getNodeAt(pathInSource);
+        SOURCE_TIMER.stop();
+        // The name should match ...
+        Path fedPath = fedNode.getLocation().getPath();
+        Path sourcePath = sourceNode.getLocation().getPath();
+        if (!fedPath.isRoot() && !sourcePath.isRoot()) {
+            assertThat(fedNode.getLocation().getPath().getLastSegment().getName(),
+                       is(sourceNode.getLocation().getPath().getLastSegment().getName()));
+        }
+        // The children should match ...
+        List<Path.Segment> fedChildren = new ArrayList<Path.Segment>();
+        List<Path.Segment> sourceChildren = new ArrayList<Path.Segment>();
+        for (Location child : fedNode.getChildren()) {
+            fedChildren.add(child.getPath().getLastSegment());
+        }
+        for (Location child : sourceNode.getChildren()) {
+            sourceChildren.add(child.getPath().getLastSegment());
+        }
+        // Add any extra children to the 'sourceChildren' ...
+        for (String extraChild : extraChildren) {
+            sourceChildren.add(context.getValueFactories().getPathFactory().createSegment(extraChild));
+        }
+        assertThat(fedChildren, is(sourceChildren));
+        // The properties should match ...
+        Map<Name, Property> fedProps = fedNode.getPropertiesByName();
+        Map<Name, Property> sourceProps = sourceNode.getPropertiesByName();
+        assertThat(fedProps, is(sourceProps));
+
+        // Now, try to get the children only ...
+        FEDERATED_TIMER.start();
+        List<Location> children = federated.getChildren().of(pathInFederated);
+        FEDERATED_TIMER.stop();
+        fedChildren.clear();
+        for (Location child : children) {
+            fedChildren.add(child.getPath().getLastSegment());
+        }
+        assertThat(fedChildren, is(sourceChildren));
+
+        // And try to get the properties only ...
+        FEDERATED_TIMER.start();
+        fedProps = federated.getPropertiesByName().on(pathInFederated);
+        FEDERATED_TIMER.stop();
+        assertThat(fedProps, is(sourceProps));
+
+        // And try to get the properties one by one ...
+        for (Property sourceProp : sourceProps.values()) {
+            FEDERATED_TIMER.start();
+            Property fedProp = federated.getProperty(sourceProp.getName()).on(pathInFederated);
+            FEDERATED_TIMER.stop();
+            assertThat(fedProp, is(sourceProp));
+        }
+
+        // Try reading a subgraph of depth 2 ...
+        FEDERATED_TIMER.start();
+        Subgraph fedSubgraph = federated.getSubgraphOfDepth(2).at(pathInFederated);
+        FEDERATED_TIMER.stop();
+        SOURCE_TIMER.start();
+        Subgraph sourceSubgraph = graphFor(sourceName, workspaceName).getSubgraphOfDepth(2).at(pathInSource);
+        SOURCE_TIMER.stop();
+        if (extraChildren.length == 0) {
+            // Can only compare the graphs when there are no extra children ...
+            AbstractConnectorTest.assertEquivalentSubgraphs(fedSubgraph, sourceSubgraph, true, false);
+        }
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractProjectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractProjectorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractProjectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,116 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+import org.jboss.dna.graph.property.Path;
+import org.junit.Before;
+
+/**
+ * @param <ProjectorType>
+ */
+public abstract class AbstractProjectorTest<ProjectorType extends Projector> {
+
+    protected ExecutionContext context;
+    protected List<Projection> projections;
+    protected ProjectorType projector;
+    protected Location locationA;
+    protected Location locationB;
+    protected Location locationAB;
+    protected Location locationABCD;
+
+    @Before
+    public void beforeEach() {
+        this.context = new ExecutionContext();
+        this.projections = new ArrayList<Projection>();
+        this.locationA = Location.create(path("/a"));
+        this.locationB = Location.create(path("/b"));
+        this.locationAB = Location.create(path("/a/b"));
+        this.locationABCD = Location.create(path("/a/b/c/d"));
+    }
+
+    protected Rule[] rule( String... rule ) {
+        Rule[] rules = new Rule[rule.length];
+        for (int i = 0; i != rule.length; ++i) {
+            rules[i] = Projection.fromString(rule[i], context);
+        }
+        return rules;
+    }
+
+    protected Path path( String path ) {
+        return context.getValueFactories().getPathFactory().create(path);
+    }
+
+    protected Path path( Path parent,
+                         String relativePath ) {
+        return context.getValueFactories().getPathFactory().create(parent, relativePath);
+    }
+
+    protected void addProjection( String sourceName,
+                                  String workspaceName,
+                                  String... rules ) {
+        projections.add(new Projection(sourceName, workspaceName, false, rule(rules)));
+    }
+
+    protected Path.Segment segment( String segment ) {
+        return context.getValueFactories().getPathFactory().createSegment(segment);
+    }
+
+    protected Location location( String path ) {
+        return Location.create(path(path));
+    }
+
+    protected Location location( UUID uuid ) {
+        return Location.create(uuid);
+    }
+
+    protected void assertNoProjectedNodeAt( String path ) {
+        ProjectedNode node = projector.project(context, location(path), false);
+        assertThat(node, is(nullValue()));
+    }
+
+    protected void assertPlacholderHasChildren( String parent,
+                                                String... childSegments ) {
+        Path parentPath = path(parent);
+        ProjectedNode node = projector.project(context, Location.create(parentPath), false);
+        assertThat(node, is(notNullValue()));
+        assertThat(node.isPlaceholder(), is(true));
+        PlaceholderNode placeholder = node.asPlaceholder();
+        List<Location> locations = new ArrayList<Location>();
+        for (String childSegment : childSegments) {
+            Path childPath = path(parentPath, childSegment);
+            locations.add(Location.create(childPath));
+        }
+        assertThat(placeholder.children(), is(locations));
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/AbstractProjectorTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjectorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,147 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.jboss.dna.graph.Location;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class BranchedMirrorProjectorTest extends AbstractProjectorTest<BranchedMirrorProjector> {
+
+    private String mirrorSourceName;
+    private String mirrorWorkspaceName;
+    private String branchSourceName;
+    private String branchWorkspaceName;
+
+    @Before
+    @Override
+    public void beforeEach() {
+        super.beforeEach();
+        this.mirrorSourceName = "source1";
+        this.mirrorWorkspaceName = "workspace1";
+        this.branchSourceName = "source2";
+        this.branchWorkspaceName = "workspace2";
+        addProjection(mirrorSourceName, mirrorWorkspaceName, "/ => /");
+        addProjection(branchSourceName, branchWorkspaceName, "/system => /system");
+        this.projector = BranchedMirrorProjector.with(context, projections);
+    }
+
+    protected void assertProjectedIntoMirror( String federatedPath ) {
+        Location location = location(federatedPath);
+        ProjectedNode node = projector.project(context, location, false);
+        assertThat(node.isProxy(), is(true));
+        ProxyNode proxy = node.asProxy();
+        assertThat(proxy.location(), is(location));
+        assertThat(proxy.source(), is(mirrorSourceName));
+        assertThat(proxy.workspaceName(), is(mirrorWorkspaceName));
+        assertThat(proxy.hasNext(), is(false));
+    }
+
+    protected void assertProjectedIntoBranch( String federatedPath ) {
+        assertProjectedIntoBranch(federatedPath, federatedPath);
+    }
+
+    protected void assertProjectedIntoBranch( String federatedPath,
+                                              String expectedPathInSource ) {
+        Location location = location(federatedPath);
+        ProjectedNode node = projector.project(context, location, false);
+        assertThat(node.isProxy(), is(true));
+        ProxyNode proxy = node.asProxy();
+        assertThat(proxy.source(), is(branchSourceName));
+        assertThat(proxy.workspaceName(), is(branchWorkspaceName));
+        assertThat(proxy.location().getPath(), is(path(expectedPathInSource)));
+        assertThat(proxy.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationInMirrorSource() {
+        assertProjectedIntoMirror("/a");
+        assertProjectedIntoMirror("/a/b/c/d");
+        assertProjectedIntoMirror("/system[2]/b/c/d"); // technically not in the branch!
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationInBranchOnlyOneLevelDeepWithOneForOneMapping() {
+        assertProjectedIntoBranch("/system");
+        assertProjectedIntoBranch("/system[1]");
+        assertProjectedIntoBranch("/system/d");
+        assertProjectedIntoBranch("/system[1]/d");
+    }
+
+    @Test
+    public void shouldReturnForRootLocationAProxyForMirrorAndPlaceholderForBranch() {
+        Location location = location("/");
+        ProjectedNode node = projector.project(context, location, false);
+        assertThat(node.isProxy(), is(true));
+        ProxyNode proxy = node.asProxy();
+        assertThat(proxy.source(), is(mirrorSourceName));
+        assertThat(proxy.workspaceName(), is(mirrorWorkspaceName));
+        assertThat(proxy.location().getPath(), is(path("/")));
+        assertThat(proxy.hasNext(), is(true));
+
+        ProjectedNode next = node.next();
+        assertThat(next.isPlaceholder(), is(true));
+        PlaceholderNode placeholder = next.asPlaceholder();
+        assertThat(placeholder.isPlaceholder(), is(true));
+        assertThat(placeholder.location().getPath(), is(path("/")));
+        assertThat(placeholder.children().size(), is(1));
+        assertThat(placeholder.children().get(0).getPath(), is(path("/system")));
+        assertThat(placeholder.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationInBranchMultipleLevelsDeepWithOneForOneMapping() {
+        projections.clear();
+        addProjection(mirrorSourceName, mirrorWorkspaceName, "/ => /");
+        addProjection(branchSourceName, branchWorkspaceName, "/a/b/c => /a/b/c");
+        this.projector = BranchedMirrorProjector.with(context, projections);
+        assertProjectedIntoBranch("/a/b/c");
+        assertProjectedIntoBranch("/a/b/c/d");
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationInBranchMultipleLevelsDeepWithDissimilarMapping() {
+        projections.clear();
+        addProjection(mirrorSourceName, mirrorWorkspaceName, "/ => /");
+        addProjection(branchSourceName, branchWorkspaceName, "/a/b/c => /d/e");
+        this.projector = BranchedMirrorProjector.with(context, projections);
+        assertProjectedIntoBranch("/a/b/c", "/d/e");
+        assertProjectedIntoBranch("/a/b/c/f", "/d/e/f");
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationInBranchMultipleLevelsDeepWithRootMapping() {
+        projections.clear();
+        addProjection(mirrorSourceName, mirrorWorkspaceName, "/ => /");
+        addProjection(branchSourceName, branchWorkspaceName, "/a/b/c => /");
+        this.projector = BranchedMirrorProjector.with(context, projections);
+        assertProjectedIntoBranch("/a/b/c", "/");
+        assertProjectedIntoBranch("/a/b/c/f", "/f");
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/BranchedMirrorProjectorTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnectionTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnectionTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnectionTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,108 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.request.CompositeRequest;
+import org.jboss.dna.graph.request.Request;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class FederatedRepositoryConnectionTest {
+
+    private FederatedRepositoryConnection connection;
+    @Mock
+    private FederatedRepository repository;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        connection = new FederatedRepositoryConnection(repository);
+    }
+
+    @Test
+    public void shouldHaveRepository() {
+        assertThat(connection.getRepository(), is(repository));
+    }
+
+    @Test
+    public void shouldHaveNoXaResource() {
+        assertThat(connection.getXAResource(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldGetDefaultCachePolicyFromRepository() {
+        CachePolicy cachePolicy = mock(CachePolicy.class);
+        stub(repository.getDefaultCachePolicy()).toReturn(cachePolicy);
+        assertThat(connection.getDefaultCachePolicy(), is(cachePolicy));
+        verify(repository, times(1)).getDefaultCachePolicy();
+    }
+
+    @Test
+    public void shouldGetSourceNameFromRepository() {
+        String name = "Something";
+        stub(repository.getSourceName()).toReturn(name);
+        assertThat(connection.getSourceName(), is(name));
+        verify(repository, times(1)).getSourceName();
+    }
+
+    @Test
+    public void shouldProcessNonCompositeRequestSynchronously() {
+        Request request = mock(Request.class);
+        assertThat(connection.shouldProcessSynchronously(request), is(true));
+    }
+
+    @Test
+    public void shouldProcessCompositeRequestWithOneRequestSynchronously() {
+        CompositeRequest request = mock(CompositeRequest.class);
+        stub(request.size()).toReturn(1);
+        assertThat(connection.shouldProcessSynchronously(request), is(true));
+    }
+
+    @Test
+    public void shouldProcessCompositeRequestWithMultipleRequestsAsynchronously() {
+        CompositeRequest request = mock(CompositeRequest.class);
+        stub(request.size()).toReturn(2);
+        assertThat(connection.shouldProcessSynchronously(request), is(false));
+    }
+
+    @Test
+    public void shouldAllowCloseToBeCalledRepeatedly() {
+        for (int i = 0; i != 10; ++i) {
+            connection.close();
+        }
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryConnectionTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,242 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.RefAddr;
+import javax.naming.Reference;
+import javax.naming.spi.ObjectFactory;
+import javax.security.auth.callback.CallbackHandler;
+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.RepositoryContext;
+import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class FederatedRepositorySourceTest {
+
+    private FederatedRepositorySource source;
+    private String sourceName;
+    private String repositoryName;
+    private String configurationSourceName;
+    private InMemoryRepositorySource configRepositorySource;
+    private RepositoryConnection configRepositoryConnection;
+    private ExecutionContext context;
+    @Mock
+    private RepositoryConnection connection;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+    @Mock
+    private RepositoryContext repositoryContext;
+
+    /**
+     * @throws java.lang.Exception
+     */
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        context = new ExecutionContext();
+        configurationSourceName = "configuration";
+        repositoryName = "Test Repository";
+        source = new FederatedRepositorySource();
+        source.setName(repositoryName);
+        sourceName = "federated source";
+        source.setName(sourceName);
+        source.setConfigurationSourceName(configurationSourceName);
+        source.setConfigurationWorkspaceName("configSpace");
+        source.setConfigurationPath("/a/b/Test Repository");
+        source.initialize(repositoryContext);
+        configRepositorySource = new InMemoryRepositorySource();
+        configRepositorySource.setName("Configuration Repository");
+
+        Graph configRepository = Graph.create(configRepositorySource, context);
+        configRepository.createWorkspace().named("configSpace");
+        Graph.Batch batch = configRepository.batch();
+        batch.create("/a").and();
+        batch.create("/a/b").and();
+        batch.create("/a/b/Test Repository").with(FederatedLexicon.DEFAULT_WORKSPACE_NAME, "fedSpace").and();
+        batch.create("/a/b/Test Repository/dna:workspaces").and();
+        batch.create("/a/b/Test Repository/dna:workspaces/fedSpace").and();
+        batch.create("/a/b/Test Repository/dna:workspaces/fedSpace/dna:cache").with(FederatedLexicon.PROJECTION_RULES, "/ => /").with(FederatedLexicon.SOURCE_NAME,
+                                                                                                                                      "cache source").with(FederatedLexicon.WORKSPACE_NAME,
+                                                                                                                                                           "cacheSpace").with(FederatedLexicon.TIME_TO_EXPIRE,
+                                                                                                                                                                              100000).and();
+        batch.create("/a/b/Test Repository/dna:workspaces/fedSpace/dna:projections").and();
+        batch.create("/a/b/Test Repository/dna:workspaces/fedSpace/dna:projections/projection1").with(FederatedLexicon.PROJECTION_RULES,
+                                                                                                      "/ => /s1").with(FederatedLexicon.SOURCE_NAME,
+                                                                                                                       "source 1").with(FederatedLexicon.WORKSPACE_NAME,
+                                                                                                                                        "s1 workspace").and();
+        batch.create("/a/b/Test Repository/dna:workspaces/fedSpace/dna:projections/projection2").with(FederatedLexicon.PROJECTION_RULES,
+                                                                                                      "/ => /s2").with(FederatedLexicon.SOURCE_NAME,
+                                                                                                                       "source 2").with(FederatedLexicon.WORKSPACE_NAME,
+                                                                                                                                        "s2 worskspace").and();
+        batch.execute();
+
+        configRepositoryConnection = configRepositorySource.getConnection();
+        stub(repositoryContext.getExecutionContext()).toReturn(context);
+        stub(repositoryContext.getRepositoryConnectionFactory()).toReturn(connectionFactory);
+        stub(connectionFactory.createConnection(configurationSourceName)).toReturn(configRepositoryConnection);
+    }
+
+    protected static CallbackHandler anyCallbackHandler() {
+        return argThat(new ArgumentMatcher<CallbackHandler>() {
+            @Override
+            public boolean matches( Object callback ) {
+                return callback != null;
+            }
+        });
+    }
+
+    @Test
+    public void shouldReturnNonNullCapabilities() {
+        assertThat(source.getCapabilities(), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldSupportSameNameSiblings() {
+        assertThat(source.getCapabilities().supportsSameNameSiblings(), is(true));
+    }
+
+    @Test
+    public void shouldSupportUpdates() {
+        assertThat(source.getCapabilities().supportsUpdates(), is(true));
+    }
+
+    @Test
+    public void shouldCreateConnectionsByAuthenticateUsingFederationRepository() throws Exception {
+        connection = source.getConnection();
+        assertThat(connection, is(notNullValue()));
+    }
+
+    @Test
+    public void shouldHaveNullSourceNameUponConstruction() {
+        source = new FederatedRepositorySource();
+        assertThat(source.getName(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldAllowSettingName() {
+        source.setName("Something");
+        assertThat(source.getName(), is("Something"));
+        source.setName("another name");
+        assertThat(source.getName(), is("another name"));
+    }
+
+    @Test
+    public void shouldAllowSettingNameToNull() {
+        source.setName("some name");
+        source.setName(null);
+        assertThat(source.getName(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldHaveDefaultRetryLimit() {
+        assertThat(source.getRetryLimit(), is(FederatedRepositorySource.DEFAULT_RETRY_LIMIT));
+    }
+
+    @Test
+    public void shouldSetRetryLimitToZeroWhenSetWithNonPositiveValue() {
+        source.setRetryLimit(0);
+        assertThat(source.getRetryLimit(), is(0));
+        source.setRetryLimit(-1);
+        assertThat(source.getRetryLimit(), is(0));
+        source.setRetryLimit(-100);
+        assertThat(source.getRetryLimit(), is(0));
+    }
+
+    @Test
+    public void shouldAllowRetryLimitToBeSet() {
+        for (int i = 0; i != 100; ++i) {
+            source.setRetryLimit(i);
+            assertThat(source.getRetryLimit(), is(i));
+        }
+    }
+
+    @Test
+    public void shouldCreateJndiReferenceAndRecreatedObjectFromReference() throws Exception {
+        int retryLimit = 100;
+        source.setRetryLimit(retryLimit);
+        source.setName("Some source");
+        source.setConfigurationSourceName("config source");
+        source.setConfigurationPath("/a/b/c");
+
+        Reference ref = source.getReference();
+        assertThat(ref.getClassName(), is(FederatedRepositorySource.class.getName()));
+        assertThat(ref.getFactoryClassName(), is(FederatedRepositorySource.class.getName()));
+
+        Map<String, Object> refAttributes = new HashMap<String, Object>();
+        Enumeration<RefAddr> enumeration = ref.getAll();
+        while (enumeration.hasMoreElements()) {
+            RefAddr addr = enumeration.nextElement();
+            refAttributes.put(addr.getType(), addr.getContent());
+        }
+
+        assertThat((String)refAttributes.remove(FederatedRepositorySource.SOURCE_NAME), is(source.getName()));
+        assertThat((String)refAttributes.remove(FederatedRepositorySource.RETRY_LIMIT), is(Integer.toString(retryLimit)));
+        assertThat((String)refAttributes.remove(FederatedRepositorySource.CONFIGURATION_WORKSPACE_NAME),
+                   is(source.getConfigurationWorkspaceName()));
+        assertThat((String)refAttributes.remove(FederatedRepositorySource.CONFIGURATION_SOURCE_NAME),
+                   is(source.getConfigurationSourceName()));
+        assertThat((String)refAttributes.remove(FederatedRepositorySource.CONFIGURATION_PATH), is("/a/b/c/"));
+        assertThat(refAttributes.isEmpty(), is(true));
+
+        // Recreate the object, use a newly constructed source ...
+        ObjectFactory factory = new FederatedRepositorySource();
+        Name name = mock(Name.class);
+        Context context = mock(Context.class);
+        Hashtable<?, ?> env = new Hashtable<Object, Object>();
+        FederatedRepositorySource recoveredSource = (FederatedRepositorySource)factory.getObjectInstance(ref, name, context, env);
+        assertThat(recoveredSource, is(notNullValue()));
+
+        assertThat(recoveredSource.getName(), is(source.getName()));
+        assertThat(recoveredSource.getRetryLimit(), is(source.getRetryLimit()));
+        assertThat(recoveredSource.getConfigurationSourceName(), is(source.getConfigurationSourceName()));
+        assertThat(recoveredSource.getConfigurationWorkspaceName(), is(source.getConfigurationWorkspaceName()));
+        assertThat(recoveredSource.getConfigurationPath(), is(source.getConfigurationPath()));
+
+        assertThat(recoveredSource.equals(source), is(true));
+        assertThat(source.equals(recoveredSource), is(true));
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,215 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import java.util.Set;
+import org.jboss.dna.graph.Graph;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * An integration test that verifies the behavior of a {@link FederatedRepositorySource} configured with a federated workspace
+ * using a single mirror projection to an underlying source.
+ */
+public class FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest
+    extends AbstractFederatedRepositorySourceIntegrationTest {
+
+    private String mirrorSourceName;
+    private String mirrorWorkspaceName;
+    private String branchSourceName;
+    private String branchWorkspaceName;
+
+    @Override
+    @Before
+    public void beforeEach() throws Exception {
+        super.beforeEach();
+
+        // Set up the mirror projection ...
+        mirrorSourceName = "Mirror Source";
+        mirrorWorkspaceName = "Mirror Workspace";
+        addProjection("fedSpace", "Mirror Projection", mirrorSourceName, mirrorWorkspaceName, "/ => /");
+
+        // Set up the branch projection ...
+        branchSourceName = mirrorSourceName;
+        branchWorkspaceName = "Branch Workspace";
+        addProjection("fedSpace", "Branch Projection", branchSourceName, branchWorkspaceName, "/Aircraft => /Aircraft");
+
+        // Add some data to the mirror source ...
+        Graph source = graphFor(mirrorSourceName, mirrorWorkspaceName);
+        source.importXmlFrom(getClass().getClassLoader().getResource("cars.xml").toURI()).into("/");
+
+        // Add some data to the mirror source ...
+        Graph branch = graphFor(branchSourceName, branchWorkspaceName);
+        branch.importXmlFrom(getClass().getClassLoader().getResource("aircraft.xml").toURI()).into("/");
+    }
+
+    /**
+     * Assert that the node in the federated repository given by the supplied path represents the same node in the underlying
+     * mirror source given by the same path.
+     * 
+     * @param pathToNode the path to the node in the federated repository and in the underlying source
+     */
+    protected void assertMirrorNode( String pathToNode ) {
+        assertSameNode(pathToNode, pathToNode, mirrorSourceName, mirrorWorkspaceName);
+    }
+
+    /**
+     * Assert that the node in the federated repository given by the supplied path represents the same node in the underlying
+     * branch source given by the same path.
+     * 
+     * @param pathToNode the path to the node in the federated repository and in the underlying source
+     */
+    protected void assertBranchNode( String pathToNode ) {
+        assertThat(pathToNode.startsWith("/Aircraft"), is(true));
+        assertSameNode(pathToNode, pathToNode, branchSourceName, branchWorkspaceName);
+    }
+
+    /**
+     * Assert that the node does not exist in the federated repository nor in the mirror source or branch source.
+     * 
+     * @param pathToNode the path to the node in the federated repository and in the underlying source
+     */
+    protected void assertNoNode( String pathToNode ) {
+        assertNoNode(pathToNode, pathToNode, mirrorSourceName, mirrorWorkspaceName);
+        assertNoNode(pathToNode, pathToNode, branchSourceName, branchWorkspaceName);
+    }
+
+    @Test
+    public void shouldListAllFederatedWorkspaces() {
+        Set<String> workspaces = federated.getWorkspaces();
+        assertThat(workspaces.contains("fedSpace"), is(true));
+        assertThat(workspaces.size(), is(1));
+    }
+
+    @Test
+    public void shouldFederateRootNodeFromMirrorAndBranch() {
+        // The root of the federated repository should have the content from the mirror but should have
+        // an additional child that is the root of the branch ...
+        assertSameNode("/", "/", mirrorSourceName, mirrorWorkspaceName, "Aircraft");
+    }
+
+    @Test
+    public void shouldFederateNodesInMirrorSource() {
+        assertMirrorNode("/Cars");
+        assertMirrorNode("/Cars/Hybrid");
+        assertMirrorNode("/Cars/Hybrid/Toyota Prius");
+        assertMirrorNode("/Cars/Hybrid/Toyota Highlander");
+        assertMirrorNode("/Cars/Hybrid/Nissan Altima");
+        assertMirrorNode("/Cars/Sports/Aston Martin DB9");
+        assertMirrorNode("/Cars/Sports/Infiniti G37");
+        assertMirrorNode("/Cars/Luxury/Cadillac DTS");
+        assertMirrorNode("/Cars/Luxury/Bentley Continental");
+        assertMirrorNode("/Cars/Luxury/Lexus IS350");
+        assertMirrorNode("/Cars/Utility/Land Rover LR2");
+        assertMirrorNode("/Cars/Utility/Land Rover LR3");
+        assertMirrorNode("/Cars/Utility/Hummer H3");
+        assertMirrorNode("/Cars/Utility/Ford F-150");
+    }
+
+    @Test
+    public void shouldFederateNodesInBranchSource() {
+        assertBranchNode("/Aircraft");
+        assertBranchNode("/Aircraft/Business");
+        assertBranchNode("/Aircraft/Business/Gulfstream V");
+        assertBranchNode("/Aircraft/Business/Learjet 45");
+    }
+
+    @Test
+    public void shouldCreateNodeUnderRootInMirrorSource() {
+        federated.createAt("/Hovercraft").with("prop1", "value1").and();
+        assertMirrorNode("/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertSameNode("/", "/", mirrorSourceName, mirrorWorkspaceName, "Aircraft");
+    }
+
+    @Test
+    public void shouldCreateNodeWellBelowRootInMirrorSource() {
+        federated.createAt("/Cars/Hybrid/MyNewHybrid").with("prop1", "value1").and();
+        assertMirrorNode("/Cars/Hybrid/MyNewHybrid");
+        // And make sure the parent node is the same ...
+        assertMirrorNode("/Cars/Hybrid");
+    }
+
+    @Test
+    public void shouldCreateNodeUnderRootInBranchSource() {
+        federated.createAt("/Aircraft/Hovercraft").with("prop1", "value1").and();
+        assertBranchNode("/Aircraft/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertBranchNode("/Aircraft");
+    }
+
+    @Test
+    public void shouldCreateNodeWellBelowRootInBranchSource() {
+        federated.createAt("/Aircraft/Business/HondaJet").with("prop1", "value1").and();
+        assertBranchNode("/Aircraft/Business/HondaJet");
+        // And make sure the parent node is the same ...
+        assertBranchNode("/Aircraft/Business");
+    }
+
+    @Test
+    public void shouldDeleteNodeUnderRootInMirrorSource() {
+        // Create a new node (since there's only one node under the root) ...
+        federated.createAt("/Hovercraft").with("prop1", "value1").and();
+        assertMirrorNode("/Hovercraft");
+        assertSameNode("/", "/", mirrorSourceName, mirrorWorkspaceName, "Aircraft");
+        // Now delete it ...
+        federated.delete("/Hovercraft");
+        assertNoNode("/Hovercraft");
+    }
+
+    @Test
+    public void shouldDeleteNodeWellBelowRootInMirrorSource() {
+        federated.delete("/Cars/Luxury/Cadillac DTS");
+        assertNoNode("/Cars/Luxury/Cadillac DTS");
+        assertMirrorNode("/Cars/Luxury/Lexus IS350");
+        assertMirrorNode("/Cars/Luxury");
+    }
+
+    @Test
+    public void shouldDeleteNodeUnderRootInBranchSource() {
+        federated.delete("/Aircraft/Business");
+        assertNoNode("/Aircraft/Business");
+        assertBranchNode("/Aircraft");
+        assertBranchNode("/Aircraft/Commercial");
+    }
+
+    @Test
+    public void shouldDeleteNodeWellBelowRootInBranchSource() {
+        federated.delete("/Aircraft/Business/Learjet 45");
+        assertNoNode("/Aircraft/Business/Learjet 45");
+        assertBranchNode("/Aircraft/Business/Gulfstream V");
+        assertBranchNode("/Aircraft/Business");
+        assertBranchNode("/Aircraft/Commercial");
+    }
+
+    @Test
+    public void shouldDeleteEverythingInMirrorAndOffsetIfDeletingRoot() {
+        federated.delete("/");
+        // The branch node should still exist, since it's a placeholder ...
+        assertThat(federated.getChildren().of("/").size(), is(1));
+        assertBranchNode("/Aircraft");
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorAndBranchProjectionsTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorProjectionTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorProjectionTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorProjectionTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,141 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import java.util.Set;
+import org.jboss.dna.graph.Graph;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * An integration test that verifies the behavior of a {@link FederatedRepositorySource} configured with a federated workspace
+ * using a single mirror projection to an underlying source.
+ */
+public class FederatedRepositorySourceUsingMirrorProjectionTest extends AbstractFederatedRepositorySourceIntegrationTest {
+
+    private String mirrorSourceName;
+    private String mirrorWorkspaceName;
+
+    @Override
+    @Before
+    public void beforeEach() throws Exception {
+        super.beforeEach();
+
+        // Set up the projection ...
+        mirrorSourceName = "Mirror Source";
+        mirrorWorkspaceName = "Mirror Workspace";
+        addProjection("fedSpace", "Mirror Projection", mirrorSourceName, mirrorWorkspaceName, "/ => /");
+
+        // Add some data to the source ...
+        Graph source = graphFor(mirrorSourceName, mirrorWorkspaceName);
+        source.importXmlFrom(getClass().getClassLoader().getResource("cars.xml").toURI()).into("/");
+    }
+
+    /**
+     * Assert that the node in the federated repository given by the supplied path represents the same node in the underlying
+     * mirror source given by the same path.
+     * 
+     * @param pathToNode the path to the node in the federated repository and in the underlying source
+     */
+    protected void assertSameNode( String pathToNode ) {
+        assertSameNode(pathToNode, pathToNode, mirrorSourceName, mirrorWorkspaceName);
+    }
+
+    /**
+     * Assert that the node in the source repository given by the supplied path does not exist in the source or in the federated
+     * repository.
+     * 
+     * @param pathToNode the path to the node in the federated repository and in the underlying source
+     */
+    protected void assertNoNode( String pathToNode ) {
+        assertNoNode(pathToNode, pathToNode, mirrorSourceName, mirrorWorkspaceName);
+    }
+
+    @Test
+    public void shouldListAllFederatedWorkspaces() {
+        Set<String> workspaces = federated.getWorkspaces();
+        assertThat(workspaces.contains("fedSpace"), is(true));
+        assertThat(workspaces.size(), is(1));
+    }
+
+    @Test
+    public void shouldFederateNodesInMirrorSource() {
+        assertSameNode("/");
+        assertSameNode("/Cars");
+        assertSameNode("/Cars/Hybrid");
+        assertSameNode("/Cars/Hybrid/Toyota Prius");
+        assertSameNode("/Cars/Hybrid/Toyota Highlander");
+        assertSameNode("/Cars/Hybrid/Nissan Altima");
+        assertSameNode("/Cars/Sports/Aston Martin DB9");
+        assertSameNode("/Cars/Sports/Infiniti G37");
+        assertSameNode("/Cars/Luxury/Cadillac DTS");
+        assertSameNode("/Cars/Luxury/Bentley Continental");
+        assertSameNode("/Cars/Luxury/Lexus IS350");
+        assertSameNode("/Cars/Utility/Land Rover LR2");
+        assertSameNode("/Cars/Utility/Land Rover LR3");
+        assertSameNode("/Cars/Utility/Hummer H3");
+        assertSameNode("/Cars/Utility/Ford F-150");
+    }
+
+    @Test
+    public void shouldCreateNodeUnderRootInMirrorSource() {
+        federated.createAt("/Hovercraft").with("prop1", "value1").and();
+        assertSameNode("/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertSameNode("/");
+    }
+
+    @Test
+    public void shouldCreateNodeWellBelowRootInMirrorSource() {
+        federated.createAt("/Cars/Hybrid/MyNewHybrid").with("prop1", "value1").and();
+        assertSameNode("/Cars/Hybrid/MyNewHybrid");
+        // And make sure the parent node is the same ...
+        assertSameNode("/Cars/Hybrid");
+    }
+
+    @Test
+    public void shouldDeleteNodeUnderRootFromMirrorSource() {
+        // Create a node that we can delete ...
+        federated.createAt("/Hovercraft").with("prop1", "value1").and();
+        assertSameNode("/Hovercraft");
+        federated.delete("/Hovercraft");
+        assertNoNode("/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertSameNode("/");
+        // Delete the cars node (which is everything) ...
+        federated.delete("/Cars");
+        // And make sure the parent node is the same ...
+        assertSameNode("/");
+    }
+
+    @Test
+    public void shouldDeleteNodeWellBelowRootFromMirrorSource() {
+        federated.delete("/Cars/Luxury/Cadillac DTS");
+        assertNoNode("/Cars/Luxury/Cadillac DTS");
+        // And make sure the parent node is the same ...
+        assertSameNode("/Cars/Luxury");
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingMirrorProjectionTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingOffsetProjectionTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingOffsetProjectionTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingOffsetProjectionTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,177 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import java.util.Set;
+import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.request.UnsupportedRequestException;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * An integration test that verifies the behavior of a {@link FederatedRepositorySource} configured with a federated workspace
+ * using a single mirror projection to an underlying source.
+ */
+public class FederatedRepositorySourceUsingOffsetProjectionTest extends AbstractFederatedRepositorySourceIntegrationTest {
+
+    private String offsetSourceName;
+    private String offsetWorkspaceName;
+
+    @Override
+    @Before
+    public void beforeEach() throws Exception {
+        super.beforeEach();
+
+        // Set up the projection ...
+        offsetSourceName = "Offset Source";
+        offsetWorkspaceName = "Offset Workspace";
+        addProjection("fedSpace", "Offset Projection", offsetSourceName, offsetWorkspaceName, "/a/b => /");
+
+        // Add some data to the source ...
+        Graph source = graphFor(offsetSourceName, offsetWorkspaceName);
+        source.importXmlFrom(getClass().getClassLoader().getResource("cars.xml").toURI()).into("/");
+    }
+
+    /**
+     * Assert that the node in the source repository given by the supplied path represents the equivalent offset node in the
+     * federated repository given by a path offset from the supplied path.
+     * 
+     * @param pathToSourceNode the path to the node in the source repository
+     */
+    protected void assertSameNode( String pathToSourceNode ) {
+        String pathToFedNode = "/a/b" + pathToSourceNode;
+        assertSameNode(pathToFedNode, pathToSourceNode, offsetSourceName, offsetWorkspaceName);
+    }
+
+    /**
+     * Assert that the node in the source repository given by the supplied path does not exist in the source and that the
+     * equivalent offset node in the federated repository also does not exist.
+     * 
+     * @param pathToSourceNode the path to the node in the source repository
+     */
+    protected void assertNoNode( String pathToSourceNode ) {
+        String pathToFedNode = "/a/b" + pathToSourceNode;
+        assertNoNode(pathToFedNode, pathToSourceNode, offsetSourceName, offsetWorkspaceName);
+    }
+
+    @Test
+    public void shouldListAllFederatedWorkspaces() {
+        Set<String> workspaces = federated.getWorkspaces();
+        assertThat(workspaces.contains("fedSpace"), is(true));
+        assertThat(workspaces.size(), is(1));
+    }
+
+    @Test
+    public void shouldFederateNodesInOffsetSource() {
+        assertSameNode("/");
+        assertSameNode("/Cars");
+        assertSameNode("/Cars/Hybrid");
+        assertSameNode("/Cars/Hybrid/Toyota Prius");
+        assertSameNode("/Cars/Hybrid/Toyota Highlander");
+        assertSameNode("/Cars/Hybrid/Nissan Altima");
+        assertSameNode("/Cars/Sports/Aston Martin DB9");
+        assertSameNode("/Cars/Sports/Infiniti G37");
+        assertSameNode("/Cars/Luxury/Cadillac DTS");
+        assertSameNode("/Cars/Luxury/Bentley Continental");
+        assertSameNode("/Cars/Luxury/Lexus IS350");
+        assertSameNode("/Cars/Utility/Land Rover LR2");
+        assertSameNode("/Cars/Utility/Land Rover LR3");
+        assertSameNode("/Cars/Utility/Hummer H3");
+        assertSameNode("/Cars/Utility/Ford F-150");
+    }
+
+    @Test( expected = UnsupportedRequestException.class )
+    public void shouldNodeAllowCreatingNodeWithinOffset() {
+        // This is not below the offset '/a/b' and should therefore fail ...
+        federated.createAt("/a/Hovercraft").with("prop1", "value1").and();
+    }
+
+    @Test
+    public void shouldCreateNodeUnderRootInOffsetSource() {
+        // Create the node below the offset ...
+        federated.createAt("/a/b/Hovercraft").with("prop1", "value1").and();
+        // Verify it is the same from the federation and source ...
+        assertSameNode("/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertSameNode("/");
+    }
+
+    @Test
+    public void shouldCreateNodeWellBelowRootInOffsetSource() {
+        // Create the node below the offset ...
+        federated.createAt("/a/b/Cars/Hybrid/MyNewHybrid").with("prop1", "value1").and();
+        // Verify it is the same from the federation and source ...
+        assertSameNode("/Cars/Hybrid/MyNewHybrid");
+        // And make sure the parent node is the same ...
+        assertSameNode("/Cars/Hybrid");
+    }
+
+    @Test
+    public void shouldAllowDeletingNodeAtBottomOfOffset() {
+        // This is not below the offset '/a/b' and should therefore fail ...
+        federated.delete("/a/b");
+        // This deletes everything, and recreates /a/b (since it is a placeholder) ...
+        assertSameNode("/");
+        assertThat(federated.getChildren().of("/a/b").size(), is(0));
+    }
+
+    @Test
+    public void shouldNodeAllowDeletingNodeWithinOffset() {
+        federated.delete("/a");
+        assertSameNode("/");
+        // All nodes should be removed, but the placeholders should remain ...
+        assertThat(federated.getChildren().of("/a").size(), is(1));
+        assertThat(federated.getChildren().of("/a/b").size(), is(0));
+    }
+
+    @Test
+    public void shouldDeleteNodeUnderRootInOffsetSource() {
+        // Create the node below the offset ...
+        federated.createAt("/a/b/Hovercraft").with("prop1", "value1").and();
+        // Verify it is the same from the federation and source ...
+        assertSameNode("/Hovercraft");
+        // And make sure the parent node is the same ...
+        assertSameNode("/");
+        // Delete the node below the offset ...
+        federated.delete("/a/b/Hovercraft");
+        // Verify it is the same from the federation and source ...
+        assertNoNode("/Hovercraft");
+        assertSameNode("/");
+        // Delete the node below the offset ...
+        federated.delete("/a/b/Cars");
+        // Verify it is the same from the federation and source ...
+        assertSameNode("/");
+    }
+
+    @Test
+    public void shouldDeleteNodeWellBelowRootInOffsetSource() {
+        // Delete the node below the offset ...
+        federated.delete("/a/b/Cars/Luxury/Cadillac DTS");
+        assertNoNode("/Cars/Luxury/Cadillac DTS");
+        // And make sure the parent node is the same ...
+        assertSameNode("/Cars/Luxury");
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositorySourceUsingOffsetProjectionTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,91 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.request.InvalidWorkspaceException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class FederatedRepositoryTest {
+
+    private String name;
+    private FederatedRepository repository;
+    private CachePolicy defaultCachePolicy;
+    private ExecutorService executor;
+    @Mock
+    private FederatedWorkspace workspace1;
+    @Mock
+    private FederatedWorkspace workspace2;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        name = "Repository";
+        executor = Executors.newSingleThreadExecutor();
+        stub(workspace1.getName()).toReturn("workspace1");
+        stub(workspace2.getName()).toReturn("workspace2");
+        Collection<FederatedWorkspace> configs = new ArrayList<FederatedWorkspace>();
+        configs.add(workspace1);
+        configs.add(workspace2);
+        repository = new FederatedRepository(name, connectionFactory, configs, defaultCachePolicy, executor);
+    }
+
+    @Test
+    public void shouldHaveNameUponCreation() {
+        assertThat(repository.getSourceName(), is(name));
+    }
+
+    @Test
+    public void shouldFindExistingWorkspaces() {
+        assertThat(repository.getWorkspace("workspace1"), is(sameInstance(workspace1)));
+        assertThat(repository.getWorkspace("workspace2"), is(sameInstance(workspace2)));
+    }
+
+    @Test
+    public void shouldFindDefaultWorkspaceIfThereIsOne() {
+        assertThat(repository.getWorkspace(null), is(sameInstance(workspace1)));
+    }
+
+    @Test( expected = InvalidWorkspaceException.class )
+    public void shouldNotFindWorkspaceThatDoesNotExist() {
+        repository.getWorkspace("non-existant");
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRepositoryTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRequestTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRequestTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRequestTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,201 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import java.util.concurrent.CountDownLatch;
+import org.jboss.dna.common.statistic.Stopwatch;
+import org.jboss.dna.graph.connector.federation.FederatedRequest.ProjectedRequest;
+import org.jboss.dna.graph.request.Request;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class FederatedRequestTest {
+
+    private FederatedRequest request;
+    @Mock
+    private Request original;
+    private Projection[] projection;
+    private Request[] projectedRequest;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        request = new FederatedRequest(original);
+        projection = new Projection[] {mock(Projection.class), mock(Projection.class), mock(Projection.class)};
+        projectedRequest = new Request[] {mock(Request.class), mock(Request.class), mock(Request.class)};
+    }
+
+    @Test
+    public void shouldHaveOriginalRequest() {
+        assertThat(request.original(), is(sameInstance(original)));
+    }
+
+    @Test
+    public void shouldAddFirstProjectedRequest() {
+        request.add(projectedRequest[0], false, false, projection[0]);
+        assertThat(request.hasIncompleteRequests(), is(true));
+        assertThat(request.getFirstProjectedRequest().getProjection(), is(sameInstance(projection[0])));
+        assertThat(request.getFirstProjectedRequest().getRequest(), is(sameInstance(projectedRequest[0])));
+        assertThat(request.getFirstProjectedRequest().hasNext(), is(false));
+        assertThat(request.getFirstProjectedRequest().next(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldAddMultipleProjectedRequests() {
+        request.add(projectedRequest[0], false, false, projection[0]);
+        request.add(projectedRequest[1], false, true, projection[1]);
+        request.add(projectedRequest[2], false, false, projection[2]);
+        assertThat(request.hasIncompleteRequests(), is(true));
+
+        ProjectedRequest first = request.getFirstProjectedRequest();
+        assertThat(first.getProjection(), is(sameInstance(projection[0])));
+        assertThat(first.getRequest(), is(sameInstance(projectedRequest[0])));
+        assertThat(first.isComplete(), is(false));
+        assertThat(first.hasNext(), is(true));
+        assertThat(first.next(), is(notNullValue()));
+
+        ProjectedRequest second = first.next();
+        assertThat(second.getProjection(), is(sameInstance(projection[1])));
+        assertThat(second.getRequest(), is(sameInstance(projectedRequest[1])));
+        assertThat(second.isComplete(), is(true));
+        assertThat(second.hasNext(), is(true));
+        assertThat(second.next(), is(notNullValue()));
+
+        ProjectedRequest third = second.next();
+        assertThat(third.getProjection(), is(sameInstance(projection[2])));
+        assertThat(third.getRequest(), is(sameInstance(projectedRequest[2])));
+        assertThat(third.isComplete(), is(false));
+        assertThat(third.hasNext(), is(false));
+        assertThat(third.next(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldAddMultipleProjectedRequestsAddedInMethodChain() {
+        request.add(projectedRequest[0], false, false, projection[0]).add(projectedRequest[1], false, true, projection[1]).add(projectedRequest[2],
+                                                                                                                               false,
+                                                                                                                               false,
+                                                                                                                               projection[2]);
+        assertThat(request.hasIncompleteRequests(), is(true));
+
+        ProjectedRequest first = request.getFirstProjectedRequest();
+        assertThat(first.getProjection(), is(sameInstance(projection[0])));
+        assertThat(first.getRequest(), is(sameInstance(projectedRequest[0])));
+        assertThat(first.isComplete(), is(false));
+        assertThat(first.hasNext(), is(true));
+        assertThat(first.next(), is(notNullValue()));
+
+        ProjectedRequest second = first.next();
+        assertThat(second.getProjection(), is(sameInstance(projection[1])));
+        assertThat(second.getRequest(), is(sameInstance(projectedRequest[1])));
+        assertThat(second.isComplete(), is(true));
+        assertThat(second.hasNext(), is(true));
+        assertThat(second.next(), is(notNullValue()));
+
+        ProjectedRequest third = second.next();
+        assertThat(third.getProjection(), is(sameInstance(projection[2])));
+        assertThat(third.getRequest(), is(sameInstance(projectedRequest[2])));
+        assertThat(third.isComplete(), is(false));
+        assertThat(third.hasNext(), is(false));
+        assertThat(third.next(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCreateCountDownLatchUponFreezeIfNumberOfIncompleteProjectedRequestsIsNonZero() {
+        request.add(projectedRequest[0], false, false, projection[0]);
+        assertThat(request.getLatch(), is(nullValue()));
+        request.freeze();
+        assertThat(request.getLatch(), is(notNullValue()));
+        assertThat(request.getLatch().getCount(), is(1L));
+        assertThat(request.hasIncompleteRequests(), is(true));
+    }
+
+    @Test
+    public void shouldNotCreateCountDownLatchUponFreezeIfNumberOfIncompleteProjectedRequestsIsZero() {
+        request.add(projectedRequest[0], false, true, projection[0]);
+        assertThat(request.getLatch(), is(nullValue()));
+        request.freeze();
+        assertThat(request.getLatch(), is(nullValue()));
+        assertThat(request.hasIncompleteRequests(), is(false));
+    }
+
+    @Test
+    public void shouldNotBlockWhenAwaitingOnRequestWithNoIncompleteRequests() throws InterruptedException {
+        request.add(projectedRequest[0], false, true, projection[0]);
+        request.freeze();
+        assertThat(request.hasIncompleteRequests(), is(false));
+        request.await();
+    }
+
+    @Test
+    public void shouldBlockWhenAwaitingOnRequestWithIncompleteRequests() throws InterruptedException {
+        request.add(projectedRequest[0], false, false, projection[0]);
+        request.add(projectedRequest[1], false, false, projection[1]);
+        request.add(projectedRequest[2], false, false, projection[2]);
+        request.freeze();
+        assertThat(request.hasIncompleteRequests(), is(true));
+        CountDownLatch latch = request.getLatch();
+        assertThat(latch.getCount(), is(3L));
+        Stopwatch sw = new Stopwatch();
+        sw.start();
+        new Thread(new CountDownRunnable(latch, 100L)).start();
+        request.await(); // this blocks until the latch reaches 0
+        assertThat(latch.getCount(), is(0L));
+        sw.stop();
+        assertThat(sw.getTotalDuration().getDurationInMilliseconds().intValue() >= 250, is(true));
+    }
+
+    protected static class CountDownRunnable implements Runnable {
+        private final CountDownLatch latch;
+        private final long sleepTimeInMillis;
+
+        protected CountDownRunnable( CountDownLatch latch,
+                                     long sleepTimeInMillis ) {
+            assert latch != null;
+            this.latch = latch;
+            this.sleepTimeInMillis = sleepTimeInMillis;
+        }
+
+        public void run() {
+            while (latch.getCount() != 0) {
+                try {
+                    Thread.sleep(sleepTimeInMillis);
+                } catch (InterruptedException err) {
+                    System.err.println("Error: interrupted while sleeping before counting down in " + this.getClass().getName());
+                }
+                latch.countDown();
+            }
+        }
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedRequestTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedWorkspaceTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedWorkspaceTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedWorkspaceTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,181 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.List;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.RepositoryContext;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class FederatedWorkspaceTest {
+
+    private FederatedWorkspace workspace;
+    private ExecutionContext context;
+    private CachePolicy cachePolicy;
+    private String sourceName;
+    private String workspaceName;
+    private List<Projection> projections;
+    @Mock
+    private RepositoryContext repositoryContext;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        sourceName = "federated";
+        workspaceName = "my workspace";
+        context = new ExecutionContext();
+        stub(repositoryContext.getExecutionContext()).toReturn(context);
+        projections = new ArrayList<Projection>();
+        projections.add(new Projection("source1", "workspace1", false, rule("/a => /a1")));
+        projections.add(new Projection("source2", "workspace2", false, rule("/a => /a2", "/b => /b")));
+        projections.add(new Projection("source2", "workspace4", false, rule("/x => /y")));
+        projections.add(new Projection("source3", "workspace3", false, rule("/c/d/e => /c1")));
+        workspace = new FederatedWorkspace(repositoryContext, sourceName, workspaceName, projections, cachePolicy);
+    }
+
+    protected Rule[] rule( String... rule ) {
+        Rule[] rules = new Rule[rule.length];
+        for (int i = 0; i != rule.length; ++i) {
+            rules[i] = Projection.fromString(rule[i], context);
+        }
+        return rules;
+    }
+
+    @Test
+    public void shouldHaveSameRepositoryContextPassedIntoConstructor() {
+        assertThat(workspace.getRepositoryContext(), is(sameInstance(repositoryContext)));
+    }
+
+    @Test
+    public void shouldHaveSameSourceNamePassedIntoConstructor() {
+        assertThat(workspace.getSourceName(), is(sameInstance(sourceName)));
+    }
+
+    @Test
+    public void shouldHaveSameWorkspaceNamePassedIntoConstructor() {
+        assertThat(workspace.getName(), is(sameInstance(workspaceName)));
+    }
+
+    @Test
+    public void shouldHaveSameCachePolicyPassedIntoContructor() {
+        assertThat(workspace.getCachePolicy(), is(sameInstance(cachePolicy)));
+    }
+
+    @Test
+    public void shouldHaveSameProjectionsPassedIntoConstructor() {
+        assertThat(workspace.getProjections(), is(projections));
+    }
+
+    @Test
+    public void shouldHaveCorrectMappingOfProjectionsBySource() {
+        assertThat(workspace.getProjectionsBySourceName().get("source1"), is(projections.subList(0, 1)));
+        assertThat(workspace.getProjectionsBySourceName().get("source2"), is(projections.subList(1, 3)));
+        assertThat(workspace.getProjectionsBySourceName().get("source3"), is(projections.subList(3, 4)));
+    }
+
+    @Test
+    public void shouldHaveProblemsContainer() {
+        assertThat(workspace.getProblems(), is(notNullValue()));
+    }
+
+    @Test
+    public void shouldCorrectlyDetermineContainmentBySourceAndWorkspaceName() {
+        assertThat(workspace.contains("source1", "workspace1"), is(true));
+        assertThat(workspace.contains("source2", "workspace2"), is(true));
+        assertThat(workspace.contains("source2", "workspace4"), is(true));
+        assertThat(workspace.contains("source3", "workspace3"), is(true));
+    }
+
+    @Test
+    public void shouldCorrectlyDetermineNonContainmentOfCaseSensitiveSourceNotInProjections() {
+        assertThat(workspace.contains("source x", "workspace3"), is(false));
+        assertThat(workspace.contains("Source1", "workspace3"), is(false));
+        assertThat(workspace.contains("", "workspace3"), is(false));
+    }
+
+    @Test
+    public void shouldCorrectlyDetermineNonContainmentOfCaseSensitiveWorkspaceNotInProjections() {
+        assertThat(workspace.contains("source1", "Workspace1"), is(false));
+        assertThat(workspace.contains("source1", "workspace 1"), is(false));
+        assertThat(workspace.contains("source1", "no workspace"), is(false));
+    }
+
+    @Test
+    public void shouldConsiderWorkspacesEqualIfSameSourceNameAndSameWorkspaceNameAndSameProjections() {
+        RepositoryContext otherReposContext = mock(RepositoryContext.class);
+        stub(otherReposContext.getExecutionContext()).toReturn(context);
+        CachePolicy otherPolicy = mock(CachePolicy.class);
+        FederatedWorkspace other = new FederatedWorkspace(otherReposContext, sourceName, workspaceName, projections, otherPolicy);
+        assertThat(workspace.equals(other), is(true));
+    }
+
+    @Test
+    public void shouldConsiderWorkspacesNotEqualIfDifferentSourceName() {
+        RepositoryContext otherReposContext = mock(RepositoryContext.class);
+        stub(otherReposContext.getExecutionContext()).toReturn(context);
+        CachePolicy otherPolicy = mock(CachePolicy.class);
+        FederatedWorkspace other = new FederatedWorkspace(otherReposContext, "diff", workspaceName, projections, otherPolicy);
+        assertThat(workspace.equals(other), is(false));
+    }
+
+    @Test
+    public void shouldConsiderWorkspacesNotEqualIfDifferentWorkspaceName() {
+        RepositoryContext otherReposContext = mock(RepositoryContext.class);
+        stub(otherReposContext.getExecutionContext()).toReturn(context);
+        CachePolicy otherPolicy = mock(CachePolicy.class);
+        FederatedWorkspace other = new FederatedWorkspace(otherReposContext, sourceName, "diff", projections, otherPolicy);
+        assertThat(workspace.equals(other), is(false));
+    }
+
+    @Test
+    public void shouldConsiderWorkspacesNotEqualIfDifferentProjections() {
+        RepositoryContext otherReposContext = mock(RepositoryContext.class);
+        stub(otherReposContext.getExecutionContext()).toReturn(context);
+        CachePolicy otherPolicy = mock(CachePolicy.class);
+        FederatedWorkspace other = new FederatedWorkspace(otherReposContext, sourceName, workspaceName,
+                                                          projections.subList(0, 3), otherPolicy);
+        assertThat(workspace.equals(other), is(false));
+    }
+
+    @Test
+    public void shouldDetermineProjection() {
+
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/FederatedWorkspaceTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorChannelTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorChannelTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorChannelTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,182 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.connector.MockRepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.federation.ForkRequestProcessor.Channel;
+import org.jboss.dna.graph.request.Request;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class ForkRequestProcessorChannelTest {
+
+    private ExecutionContext context;
+    private String sourceName;
+    private Channel channel;
+    private List<Request> requests;
+    private ExecutorService executor;
+    private LinkedList<Request> executedRequests;
+    private RepositoryConnection connection;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        context = new ExecutionContext();
+        sourceName = "SourceA";
+        channel = new ForkRequestProcessor.Channel(sourceName);
+        requests = new ArrayList<Request>();
+        requests.add(mock(Request.class));
+        requests.add(mock(Request.class));
+        requests.add(mock(Request.class));
+        requests.add(mock(Request.class));
+
+        // Create the mock connection ...
+        executedRequests = new LinkedList<Request>(); // this is where requests submitted to the connection will go
+        connection = new MockRepositoryConnection(sourceName, executedRequests);
+
+        // Stub the connection factory ...
+        stub(connectionFactory.createConnection(sourceName)).toReturn(connection);
+
+        // Create the executor ...
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @Test
+    public void shouldCreateEmptyIteratorIfDoneCalledBeforeObtainingIterator() {
+        channel.done();
+        Iterator<Request> iter = channel.createIterator();
+        assertThat(iter.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldCreateEmptyIteratorIfDoneCalledAfterObtainingIterator() {
+        Iterator<Request> iter = channel.createIterator();
+        channel.done();
+        assertThat(iter.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldCreateIteratorOverRequestsAddedToChannelAfterObtainingIterator() {
+        Iterator<Request> iter = channel.createIterator();
+        // Add the requests ...
+        for (Request request : requests) {
+            channel.add(request);
+        }
+        // Call done ...
+        channel.done();
+        // Start iterating ...
+        for (Request expected : requests) {
+            assertThat(iter.hasNext(), is(true));
+            assertThat(iter.next(), is(sameInstance(expected)));
+        }
+        assertThat(iter.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldCreateIteratorOverRequestsAddedToChannelAfterBeginningIteration() {
+        Iterator<Request> iter = channel.createIterator();
+        // Add the requests in a separate thread ...
+        new Thread(new AddRequestsRunnable(channel, requests, 100)).start();
+        // Start iterating ...
+        for (Request expected : requests) {
+            assertThat(iter.hasNext(), is(true)); // blocks
+            assertThat(iter.next(), is(sameInstance(expected)));
+        }
+        assertThat(iter.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldSubmitRequestsToConnection() throws Exception {
+        // Start the channel ...
+        channel.start(executor, context, connectionFactory);
+
+        // Submit the requests to the channel ...
+        for (Request request : requests) {
+            channel.add(request);
+        }
+
+        // Mark the channel as done ...
+        channel.done();
+
+        // Wait until the channel has completed ...
+        channel.await();
+
+        // Verify that all the requests to the channel were processed ...
+        Iterator<Request> iter = executedRequests.iterator();
+        for (Request expected : requests) {
+            assertThat(iter.hasNext(), is(true));
+            assertThat(iter.next(), is(sameInstance(expected)));
+        }
+        assertThat(iter.hasNext(), is(false));
+    }
+
+    protected static class AddRequestsRunnable implements Runnable {
+        private final Channel channel;
+        private final Iterator<Request> requests;
+        private final int intervalInMillis;
+
+        protected AddRequestsRunnable( Channel channel,
+                                       List<Request> requests,
+                                       int intervalInMillis ) {
+            this.requests = requests.iterator();
+            this.intervalInMillis = intervalInMillis;
+            this.channel = channel;
+        }
+
+        public void run() {
+            while (requests.hasNext()) {
+                try {
+                    Thread.sleep(intervalInMillis);
+                } catch (InterruptedException err) {
+                    fail(err.getMessage());
+                }
+                channel.add(requests.next());
+            }
+            // Call done ...
+            channel.done();
+        }
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorChannelTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,365 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.MockRepositoryConnection;
+import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+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.PathNotFoundException;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.property.Path.Segment;
+import org.jboss.dna.graph.request.InvalidWorkspaceException;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class ForkRequestProcessorTest {
+
+    private ForkRequestProcessor processor;
+    private ExecutionContext context;
+    private DateTime now;
+    private String sourceName;
+    private String workspaceName;
+    private String sourceNameA;
+    private String sourceNameB;
+    private String sourceNameC;
+    private String workspaceNameA;
+    private String workspaceNameB;
+    private String workspaceNameC;
+    private String nonExistantWorkspaceName;
+    private LinkedList<FederatedRequest> federatedRequests;
+    private ExecutorService executor;
+    @Mock
+    private FederatedWorkspace workspace;
+    @Mock
+    private FederatedRepository repository;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+    private Projection projectionA;
+    private Projection projectionB;
+    // private Projection projectionC;
+    private MockRepositoryConnection connectionForSourceA;
+    private MockRepositoryConnection connectionForSourceB;
+    private MockRepositoryConnection connectionForSourceC;
+    private Map<Name, Property> properties;
+    private List<Location> children;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        executor = Executors.newSingleThreadExecutor();
+        sourceName = "MySource";
+        workspaceName = "MyWorkspace";
+        sourceNameA = "SourceA";
+        sourceNameB = "SourceB";
+        sourceNameC = "SourceC";
+        workspaceNameA = "WorkspaceA";
+        workspaceNameB = "WorkspaceB";
+        workspaceNameC = "WorkspaceC";
+        nonExistantWorkspaceName = "Non-Existant Workspace";
+        context = new ExecutionContext();
+        now = context.getValueFactories().getDateFactory().create();
+        federatedRequests = new LinkedList<FederatedRequest>();
+        children = new ArrayList<Location>();
+        properties = new HashMap<Name, Property>();
+
+        // Set up the connection factory and connection ...
+        connectionForSourceA = new MockRepositoryConnection(sourceNameA);
+        connectionForSourceB = new MockRepositoryConnection(sourceNameB);
+        connectionForSourceC = new MockRepositoryConnection(sourceNameC);
+        stub(connectionFactory.createConnection(sourceNameA)).toReturn(connectionForSourceA);
+        stub(connectionFactory.createConnection(sourceNameB)).toReturn(connectionForSourceB);
+        stub(connectionFactory.createConnection(sourceNameC)).toReturn(connectionForSourceC);
+
+        // Stub the FederatedRepository ...
+        stub(repository.getSourceName()).toReturn(sourceName);
+        stub(repository.getWorkspace(workspaceName)).toReturn(workspace);
+        stub(repository.getExecutor()).toReturn(executor);
+        stub(repository.getConnectionFactory()).toReturn(connectionFactory);
+        stub(repository.getWorkspace(nonExistantWorkspaceName)).toThrow(new InvalidWorkspaceException());
+
+        // Stub the FederatedWorkspace ...
+        stub(workspace.getName()).toReturn(workspaceName);
+        // workspace.project(context,location) needs to be stubbed ...
+
+        // Set up the projections ...
+        projectionA = new Projection(sourceNameA, workspaceNameA, false, rules("/a => /"));
+        projectionB = new Projection(sourceNameB, workspaceNameB, false, rules("/b => /"));
+        // projectionC = new Projection(sourceNameC, workspaceNameC, rules("/c => /"));
+
+        // Now set up the processor ...
+        processor = new ForkRequestProcessor(repository, context, now, federatedRequests);
+    }
+
+    protected Rule[] rules( String... rule ) {
+        Rule[] rules = new Rule[rule.length];
+        for (int i = 0; i != rule.length; ++i) {
+            rules[i] = Projection.fromString(rule[i], context);
+        }
+        return rules;
+    }
+
+    public Name name( String name ) {
+        return context.getValueFactories().getNameFactory().create(name);
+    }
+
+    public Path path( String path ) {
+        return context.getValueFactories().getPathFactory().create(path);
+    }
+
+    public Segment segment( String segment ) {
+        return context.getValueFactories().getPathFactory().createSegment(segment);
+    }
+
+    public Location location( String path ) {
+        return Location.create(path(path));
+    }
+
+    public void addProperty( String name,
+                             Object... values ) {
+        Property property = context.getPropertyFactory().create(name(name), values);
+        properties.put(property.getName(), property);
+    }
+
+    public void addChild( Location parent,
+                          String childName ) {
+        Path path = context.getValueFactories().getPathFactory().create(parent.getPath(), segment(childName));
+        children.add(Location.create(path));
+    }
+
+    @Test
+    public void shouldReturnImmediatelyFromAwaitIfNoChannelsAreMade() throws Exception {
+        processor.await();
+    }
+
+    @Test
+    public void shouldReturnFromAwaitAfterChannelsAreCompleted() throws Exception {
+        ReadNodeRequest requestA = new ReadNodeRequest(location("/a/some"), workspaceNameA);
+        ReadNodeRequest requestB = new ReadNodeRequest(location("/b/some"), workspaceNameB);
+        ReadNodeRequest requestC = new ReadNodeRequest(location("/c/some"), workspaceNameC);
+        processor.submit(requestA, sourceNameA);
+        processor.submit(requestB, sourceNameB);
+        processor.submit(requestC, sourceNameC);
+        processor.close();
+        processor.await();
+    }
+
+    @Test
+    public void shouldReturnReadableLocation() {
+        Location location = location("/dna:something/jcr:else");
+        String result = processor.readable(location("/dna:something/jcr:else"));
+        assertThat(result, is(location.getString(context.getNamespaceRegistry())));
+    }
+
+    @Test
+    public void shouldFindFederatedWorkspaceByName() {
+        ReadNodeRequest request = new ReadNodeRequest(location("/some"), this.workspace.getName());
+        FederatedWorkspace workspace = processor.getWorkspace(request, request.inWorkspace());
+        assertThat(workspace, is(sameInstance(this.workspace)));
+    }
+
+    @Test
+    public void shouldRecordErrorOnRequestIfFederatedWorkspaceCouldNotBeFoundByName() {
+        ReadNodeRequest request = new ReadNodeRequest(location("/some"), nonExistantWorkspaceName);
+        FederatedWorkspace workspace = processor.getWorkspace(request, request.inWorkspace());
+        assertThat(workspace, is(nullValue()));
+        assertThat(request.hasError(), is(true));
+        assertThat(request.getError(), is(instanceOf(InvalidWorkspaceException.class)));
+    }
+
+    @Test
+    public void shouldSubmitFederatedRequestToQueueIfFederatedRequestHasNoIncompleteRequests() {
+        FederatedRequest request = mock(FederatedRequest.class);
+        stub(request.hasIncompleteRequests()).toReturn(false);
+        processor.submit(request);
+        assertThat(federatedRequests.size(), is(1));
+        assertThat(federatedRequests.get(0), is(sameInstance(request)));
+    }
+
+    @Test
+    public void shouldSubmitToSourcesTheSingleRequestInFederatedRequestAndAddFederatedRequestToQueue() throws Exception {
+        ReadNodeRequest original = new ReadNodeRequest(location("/a/some"), this.workspace.getName());
+        FederatedRequest request = new FederatedRequest(original);
+        // Create the projection and the source request ...
+        ReadNodeRequest sourceRequest = new ReadNodeRequest(location("/some"), workspaceNameA);
+        request.add(sourceRequest, false, false, projectionA);
+        assertThat(request.getFirstProjectedRequest().getProjection(), is(sameInstance(projectionA)));
+        assertThat(request.getFirstProjectedRequest().hasNext(), is(false));
+
+        // Submit the federated request ...
+        processor.submit(request);
+        assertThat(federatedRequests.size(), is(1));
+        assertThat(federatedRequests.get(0), is(sameInstance(request)));
+        // Wait for the processor to complete all source channels ...
+        processor.close();
+        processor.await();
+        // The source should have received something like the original
+        assertThat(connectionForSourceA.getProcessedRequests().contains(sourceRequest), is(true));
+    }
+
+    @Test
+    public void shouldSubmitToSourcesTheMultipleRequestsInFederatedRequestAndAddFederatedRequestToQueue() throws Exception {
+        ReadNodeRequest original = new ReadNodeRequest(location("/some"), this.workspace.getName());
+        FederatedRequest request = new FederatedRequest(original);
+
+        // Create the first projection and the source request ...
+        ReadNodeRequest sourceRequestA = new ReadNodeRequest(location("/a/some/other"), workspaceNameA);
+        request.add(sourceRequestA, false, false, projectionA);
+        assertThat(request.getFirstProjectedRequest().getProjection(), is(sameInstance(projectionA)));
+
+        // Create the second projection and the source request ...
+        ReadNodeRequest sourceRequestB = new ReadNodeRequest(location("/b/some/other"), workspaceNameB);
+        request.add(sourceRequestB, false, false, projectionB);
+        assertThat(request.getFirstProjectedRequest().next().getProjection(), is(sameInstance(projectionB)));
+
+        assertThat(request.getFirstProjectedRequest().next().hasNext(), is(false));
+
+        // Submit the federated request ...
+        processor.submit(request);
+        assertThat(federatedRequests.size(), is(1));
+        assertThat(federatedRequests.get(0), is(sameInstance(request)));
+        // Wait for the processor to complete all source channels ...
+        processor.close();
+        processor.await();
+        // The source should have received something like the original
+        assertThat(connectionForSourceA.getProcessedRequests().contains(sourceRequestA), is(true));
+        assertThat(connectionForSourceB.getProcessedRequests().contains(sourceRequestB), is(true));
+        assertThat(connectionForSourceC.getProcessedRequests().isEmpty(), is(true));
+    }
+
+    // ----------------------------------------------------------------------------------------------------------------
+    // process(ReadNodeRequest)
+    // ----------------------------------------------------------------------------------------------------------------
+
+    @Test
+    public void shouldNotForkReadNodeRequestIfWorkspaceNameIsInvalid() {
+        // Stub the workspace to have no projection ...
+        Location locationInFed = location("/a/b");
+        stub(workspace.project(context, locationInFed, false)).toReturn(null);
+
+        ReadNodeRequest request = new ReadNodeRequest(locationInFed, nonExistantWorkspaceName);
+        processor.process(request);
+        assertThat(request.hasError(), is(true));
+        assertThat(request.getError(), is(instanceOf(InvalidWorkspaceException.class)));
+    }
+
+    @Test
+    public void shouldNotForkReadNodeRequestIfThereIsNoProjection() {
+        // Stub the workspace to have no projection ...
+        Location locationInFed = location("/a/b");
+        stub(workspace.project(context, locationInFed, false)).toReturn(null);
+
+        ReadNodeRequest request = new ReadNodeRequest(locationInFed, workspaceName);
+        processor.process(request);
+        assertThat(request.hasError(), is(true));
+        assertThat(request.getError(), is(instanceOf(PathNotFoundException.class)));
+    }
+
+    @Test
+    public void shouldSubmitSingleSourceRequestWhenProcessingSingleReadNodeRequest() throws Exception {
+        // Stub the workspace to have a projection ...
+        Location locationInFed = location("/a/x/y");
+        Location locationInSource = location("/x/y");
+        ProjectedNode projectedNode = new ProxyNode(projectionA, locationInSource, locationInFed, false);
+        stub(workspace.project(context, locationInFed, false)).toReturn(projectedNode);
+
+        ReadNodeRequest request = new ReadNodeRequest(locationInFed, workspaceName);
+        processor.process(request);
+        assertThat(request.hasError(), is(false));
+
+        // Check that the federated request has the right information ...
+        FederatedRequest fedRequest = federatedRequests.poll();
+        ReadNodeRequest projectedRequest = (ReadNodeRequest)fedRequest.getFirstProjectedRequest().getRequest();
+        assertThat(projectedRequest.at(), is(locationInSource));
+        assertThat(fedRequest.getFirstProjectedRequest().hasNext(), is(false));
+
+        // Close the processor ...
+        processor.close();
+        processor.await();
+
+        // Verify the source saw the expected read ...
+        ReadNodeRequest sourceRequest = (ReadNodeRequest)connectionForSourceA.getProcessedRequests().poll();
+        assertThat(sourceRequest.at().getPath(), is(locationInSource.getPath()));
+        assertThat(connectionForSourceB.getProcessedRequests().isEmpty(), is(true));
+        assertThat(connectionForSourceC.getProcessedRequests().isEmpty(), is(true));
+    }
+
+    @Test
+    public void shouldNotSubmitPlaceholderNodesWhenProcessingReadNodeRequest() throws Exception {
+        // Stub the workspace to have a projection ...
+        Location locationInFed = location("/a/x/y");
+        Location locationInSource = location("/a/x/y");
+        addProperty("propA", "valueA");
+        addProperty("propB", "valueB");
+        addChild(locationInSource, "child1");
+        addChild(locationInSource, "child2");
+        PlaceholderNode projectedNode = new PlaceholderNode(locationInSource, properties, children);
+        stub(workspace.project(context, locationInFed, false)).toReturn(projectedNode);
+
+        ReadNodeRequest request = new ReadNodeRequest(locationInFed, workspaceName);
+        processor.process(request);
+        assertThat(request.hasError(), is(false));
+
+        // Check that the federated request has the right information ...
+        FederatedRequest fedRequest = federatedRequests.poll();
+        ReadNodeRequest projectedRequest = (ReadNodeRequest)fedRequest.getFirstProjectedRequest().getRequest();
+        assertThat(projectedRequest.at(), is(locationInFed));
+        assertThat(projectedRequest.getChildren(), is(children));
+        assertThat(projectedRequest.getPropertiesByName(), is(properties));
+        assertThat(fedRequest.getFirstProjectedRequest().hasNext(), is(false));
+
+        // Close the processor ...
+        processor.close();
+        processor.await();
+
+        // Verify that no sources saw a request ...
+        assertThat(connectionForSourceA.getProcessedRequests().isEmpty(), is(true));
+        assertThat(connectionForSourceB.getProcessedRequests().isEmpty(), is(true));
+        assertThat(connectionForSourceC.getProcessedRequests().isEmpty(), is(true));
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessorTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Copied: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/GeneralProjectorTest.java (from rev 910, trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java)
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/GeneralProjectorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/GeneralProjectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,62 @@
+/*
+ * 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.connector.federation;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class GeneralProjectorTest extends AbstractProjectorTest<GeneralProjector> {
+
+    private String sourceNameA;
+    private String sourceNameB;
+    private String sourceNameC;
+    private String workspaceNameA;
+    private String workspaceNameB;
+    private String workspaceNameC;
+
+    @Before
+    @Override
+    public void beforeEach() {
+        super.beforeEach();
+        sourceNameA = "Source A";
+        sourceNameB = "Source B";
+        sourceNameC = "Source C";
+        workspaceNameA = "Workspace A";
+        workspaceNameB = "Workspace B";
+        workspaceNameC = "Workspace C";
+        addProjection(sourceNameA, workspaceNameA, "/a => /");
+        addProjection(sourceNameB, workspaceNameB, "/b1/b2 => /");
+        addProjection(sourceNameC, workspaceNameC, "/c1/c2/c3 => /x/y");
+        this.projector = GeneralProjector.with(context, projections);
+    }
+
+    @Test
+    public void should() {
+
+    }
+
+}

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,350 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import org.jboss.dna.graph.DnaLexicon;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.cache.CachePolicy;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+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;
+import org.jboss.dna.graph.property.Path.Segment;
+import org.jboss.dna.graph.request.ReadNodeRequest;
+import org.jboss.dna.graph.request.Request;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * 
+ */
+public class JoinRequestProcessorTest {
+
+    private JoinRequestProcessor joinProcessor;
+    private ExecutionContext context;
+    private DateTime now;
+    private String sourceName;
+    protected List<Request> unknownRequests;
+    protected BlockingQueue<FederatedRequest> joinQueue;
+    private Projection mirrorProjection;
+    private Projection projectionA;
+    @Mock
+    private FederatedRepository repository;
+    @Mock
+    private CachePolicy cachePolicy;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        sourceName = "MySource";
+        stub(repository.getDefaultCachePolicy()).toReturn(cachePolicy);
+        stub(repository.getSourceName()).toReturn(sourceName);
+
+        unknownRequests = new ArrayList<Request>();
+        context = new ExecutionContext();
+        now = context.getValueFactories().getDateFactory().create();
+        joinProcessor = new JoinRequestProcessorWithUnknownHandler(repository, context, now);
+        joinQueue = new LinkedBlockingQueue<FederatedRequest>();
+
+        // Set up the projections ...
+        mirrorProjection = new Projection("sourceA", "workspaceM", false, rules("/ => /"));
+        projectionA = new Projection("sourceA", "workspaceA", false, rules("/a => /"));
+    }
+
+    protected Rule[] rules( String... rule ) {
+        Rule[] rules = new Rule[rule.length];
+        for (int i = 0; i != rule.length; ++i) {
+            rules[i] = Projection.fromString(rule[i], context);
+        }
+        return rules;
+    }
+
+    public Name name( String name ) {
+        return context.getValueFactories().getNameFactory().create(name);
+    }
+
+    public Path path( String path ) {
+        return context.getValueFactories().getPathFactory().create(path);
+    }
+
+    public Segment segment( String segment ) {
+        return context.getValueFactories().getPathFactory().createSegment(segment);
+    }
+
+    public Location location( String path ) {
+        return Location.create(path(path));
+    }
+
+    public Property property( String name,
+                              Object... values ) {
+        return context.getPropertyFactory().create(name(name), values);
+    }
+
+    public Location child( Location parent,
+                           String childName ) {
+        Path path = context.getValueFactories().getPathFactory().create(parent.getPath(), segment(childName));
+        return Location.create(path);
+    }
+
+    public static void pause( long millis ) {
+        try {
+            Thread.sleep(100L);
+        } catch (InterruptedException e) {
+            try {
+                fail("Interrupted while sleeping");
+            } finally {
+                Thread.interrupted();
+            }
+        }
+    }
+
+    @Test
+    public void shouldProcessFederatedRequestsUsingIteratable() {
+        List<FederatedRequest> requests = new ArrayList<FederatedRequest>();
+        Request original = mock(Request.class);
+        FederatedRequest request = mock(FederatedRequest.class);
+        stub(request.original()).toReturn(original);
+        requests.add(request);
+        joinProcessor.process(requests);
+        assertThat(joinProcessor.federatedRequest, is(sameInstance(request)));
+        assertThat(unknownRequests.size(), is(1));
+        assertThat(unknownRequests.get(0), is(sameInstance(original)));
+    }
+
+    @Test
+    public void shouldProcessFederatedRequestsUsingBlockingQueue() {
+        Request original = mock(Request.class);
+        // Create the original request, the projection, and the federated request ...
+        FederatedRequest request = new FederatedRequest(original);
+        request.add(original, false, false, projectionA);
+        // Freeze the request ...
+        request.freeze();
+        // And mark it as done by decrementing the latch ...
+        request.getLatch().countDown();
+        assertThat(request.getLatch().getCount(), is(0L));
+        // Create the queue and add the request ...
+        BlockingQueue<FederatedRequest> queue = new LinkedBlockingQueue<FederatedRequest>();
+        queue.add(request);
+        // Add a terminating request ...
+        queue.add(new NoMoreFederatedRequests());
+        joinProcessor.process(queue);
+        assertThat(joinProcessor.federatedRequest, is(sameInstance(request)));
+        assertThat(unknownRequests.size(), is(1));
+        assertThat(unknownRequests.get(0), is(sameInstance(original)));
+    }
+
+    @Test
+    public void shouldProcessFederatedRequestsUsingBlockingQueueThatIsTerminatedAfterProcessingBegins() {
+        final Request original = mock(Request.class);
+        final FederatedRequest request = new FederatedRequest(original);
+        Thread thread = new Thread() {
+            @Override
+            public void run() {
+                // Create the original request, the projection, and the federated request ...
+                Projection projection = mock(Projection.class);
+                request.add(original, false, false, projection);
+                // Freeze the request ...
+                request.freeze();
+                // Add the request ...
+                joinQueue.add(request);
+                // Pause ...
+                pause(100L);
+                // And mark it as done by decrementing the latch ...
+                request.getLatch().countDown();
+                assertThat(request.getLatch().getCount(), is(0L));
+                // Pause ...
+                pause(100L);
+                // Add a terminating request ...
+                joinQueue.add(new NoMoreFederatedRequests());
+            }
+        };
+        thread.start();
+        joinProcessor.process(joinQueue);
+        assertThat(joinProcessor.federatedRequest, is(sameInstance(request)));
+        assertThat(unknownRequests.size(), is(1));
+        assertThat(unknownRequests.get(0), is(sameInstance(original)));
+    }
+
+    @Test
+    public void shouldJoinSingleMirroredReadNodeRequest() {
+        // Create the original read node request ...
+        final ReadNodeRequest original = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        // Create a single federated request...
+        final FederatedRequest request = new FederatedRequest(original);
+        // And "fork" the original request ...
+        final ReadNodeRequest projected = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        projected.setActualLocationOfNode(Location.create(projected.at().getPath(), UUID.randomUUID()));
+        projected.addProperty(property("propA", "valueA"));
+        projected.addProperty(property("propB", "valueB"));
+        projected.addChild(child(projected.getActualLocationOfNode(), "child1"));
+        projected.addChild(child(projected.getActualLocationOfNode(), "child2"));
+        request.add(projected, true, false, mirrorProjection);
+        request.freeze();
+        request.getLatch().countDown();
+        joinQueue.add(request);
+        // Add a terminating request and join the request...
+        joinQueue.add(new NoMoreFederatedRequests());
+        joinProcessor.process(joinQueue);
+        // Check the results of the original has the same results of the projected...
+        assertThat(original.getChildren(), is(projected.getChildren()));
+        assertThat(original.getPropertiesByName(), is(projected.getPropertiesByName()));
+        assertThat(original.getActualLocationOfNode(), is(projected.getActualLocationOfNode()));
+    }
+
+    @Test
+    public void shouldJoinSingleOffsetReadNodeRequest() {
+        // Create the original read node request ...
+        final ReadNodeRequest original = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        // Create a single federated request using the mirror projection...
+        final FederatedRequest request = new FederatedRequest(original);
+        // And "fork" the original request by creating a mirror
+        final ReadNodeRequest projected = new ReadNodeRequest(location("/b/c"), "workspaceM");
+        projected.setActualLocationOfNode(Location.create(projected.at().getPath(), UUID.randomUUID()));
+        projected.addProperty(property("propA", "valueA"));
+        projected.addProperty(property("propB", "valueB"));
+        projected.addChild(child(projected.getActualLocationOfNode(), "child1"));
+        projected.addChild(child(projected.getActualLocationOfNode(), "child2"));
+        request.add(projected, false, false, projectionA);
+        request.freeze();
+        request.getLatch().countDown();
+        joinQueue.add(request);
+        // Add a terminating request and join the request...
+        joinQueue.add(new NoMoreFederatedRequests());
+        joinProcessor.process(joinQueue);
+        // Check the results of the original has the same results of the projected...
+        assertThat(original.getPropertiesByName(), is(projected.getPropertiesByName()));
+        assertThat(original.getActualLocationOfNode().getPath(), is(path("/a/b/c")));
+        assertThat(original.getChildren().get(0).getPath(), is(path("/a/b/c/child1")));
+        assertThat(original.getChildren().get(1).getPath(), is(path("/a/b/c/child2")));
+    }
+
+    @Test
+    public void shouldJoinMultipleReadNodeRequest() {
+        // Create the original read node request ...
+        final ReadNodeRequest original = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        // Create a single federated request using the mirror projection...
+        final FederatedRequest request = new FederatedRequest(original);
+        // And "fork" the original request into the first source request ...
+        final ReadNodeRequest projected1 = new ReadNodeRequest(location("/b/c"), "workspaceM");
+        projected1.setActualLocationOfNode(Location.create(projected1.at().getPath(), UUID.randomUUID()));
+        projected1.addProperty(property("propA", "valueA"));
+        projected1.addProperty(property("propB", "valueB"));
+        projected1.addChild(child(projected1.getActualLocationOfNode(), "child1"));
+        projected1.addChild(child(projected1.getActualLocationOfNode(), "child2"));
+        request.add(projected1, false, false, projectionA);
+        // And a second source request ...
+        final ReadNodeRequest projected2 = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        projected2.setActualLocationOfNode(Location.create(projected2.at().getPath(), UUID.randomUUID()));
+        projected2.addProperty(property("propC", "valueC"));
+        projected2.addProperty(property("propD", "valueD"));
+        projected2.addChild(child(projected2.getActualLocationOfNode(), "child2"));
+        projected2.addChild(child(projected2.getActualLocationOfNode(), "child3"));
+        request.add(projected2, true, false, mirrorProjection);
+        request.freeze();
+        request.getLatch().countDown();
+        request.getLatch().countDown();
+        joinQueue.add(request);
+        // Add a terminating request and join the request...
+        joinQueue.add(new NoMoreFederatedRequests());
+        joinProcessor.process(joinQueue);
+        // Check the results of the original has the same results of the projected...
+        assertThat(original.getProperties().containsAll(projected1.getProperties()), is(true));
+        assertThat(original.getProperties().containsAll(projected2.getProperties()), is(true));
+        assertThat(original.getActualLocationOfNode().getPath(), is(path("/a/b/c")));
+        assertThat(original.getActualLocationOfNode().getUuid(), is(projected1.getActualLocationOfNode().getUuid()));
+        assertThat(original.getActualLocationOfNode().getIdProperty(DnaLexicon.UUID).isMultiple(), is(true));
+        assertThat(original.getActualLocationOfNode().getIdProperty(DnaLexicon.UUID).getValuesAsArray()[0],
+                   is((Object)projected1.getActualLocationOfNode().getUuid()));
+        assertThat(original.getActualLocationOfNode().getIdProperty(DnaLexicon.UUID).getValuesAsArray()[1],
+                   is((Object)projected2.getActualLocationOfNode().getUuid()));
+        assertThat(original.getChildren().get(0).getPath(), is(path("/a/b/c/child1")));
+        assertThat(original.getChildren().get(1).getPath(), is(path("/a/b/c/child2")));
+        assertThat(original.getChildren().get(2).getPath(), is(path("/a/b/c/child2[2]")));
+        assertThat(original.getChildren().get(3).getPath(), is(path("/a/b/c/child3")));
+    }
+
+    @Test
+    public void shouldCancelFederatedRequestIfOneOfSeveralMultipleReadNodeRequestIsCancelled() {
+        // Create the original read node request ...
+        final ReadNodeRequest original = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        // Create a single federated request using the mirror projection...
+        final FederatedRequest request = new FederatedRequest(original);
+        // And "fork" the original request into the first source request ...
+        final ReadNodeRequest projected1 = new ReadNodeRequest(location("/b/c"), "workspaceM");
+        projected1.setActualLocationOfNode(Location.create(projected1.at().getPath(), UUID.randomUUID()));
+        projected1.addProperty(property("propA", "valueA"));
+        projected1.addProperty(property("propB", "valueB"));
+        projected1.addChild(child(projected1.getActualLocationOfNode(), "child1"));
+        projected1.addChild(child(projected1.getActualLocationOfNode(), "child2"));
+        request.add(projected1, false, false, projectionA);
+        // And a second source request (that was cancelled)...
+        final ReadNodeRequest projected2 = new ReadNodeRequest(location("/a/b/c"), "workspaceM");
+        projected2.cancel();
+        request.add(projected2, true, false, mirrorProjection);
+        request.freeze();
+        request.getLatch().countDown();
+        request.getLatch().countDown();
+        joinQueue.add(request);
+        // Add a terminating request and join the request...
+        joinQueue.add(new NoMoreFederatedRequests());
+        joinProcessor.process(joinQueue);
+        // Check the results of the original has the same results of the projected...
+        assertThat(original.getProperties().isEmpty(), is(true));
+        assertThat(original.getChildren().isEmpty(), is(true));
+        assertThat(original.isCancelled(), is(true));
+    }
+
+    /**
+     * A specialization of {@link JoinRequestProcessor} that simply records unknown request types into
+     * {@link JoinRequestProcessorTest#unknownRequests}.
+     */
+    protected class JoinRequestProcessorWithUnknownHandler extends JoinRequestProcessor {
+        protected JoinRequestProcessorWithUnknownHandler( FederatedRepository repository,
+                                                          ExecutionContext context,
+                                                          DateTime now ) {
+            super(repository, context, now);
+        }
+
+        @Override
+        protected void processUnknownRequest( Request request ) {
+            unknownRequests.add(request);
+        }
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/JoinRequestProcessorTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Copied: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/MirrorProjectorTest.java (from rev 910, trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java)
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/MirrorProjectorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/MirrorProjectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,58 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class MirrorProjectorTest extends AbstractProjectorTest<MirrorProjector> {
+
+    private String sourceName;
+    private String workspaceName;
+
+    @Before
+    @Override
+    public void beforeEach() {
+        super.beforeEach();
+        this.sourceName = "source1";
+        this.workspaceName = "workspace1";
+        addProjection(sourceName, workspaceName, "/ => /");
+        this.projector = MirrorProjector.with(context, projections);
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForSuppliedLocation() {
+        ProjectedNode node = projector.project(context, locationA, false);
+        assertThat(node.isProxy(), is(true));
+        ProxyNode proxy = node.asProxy();
+        assertThat(proxy.location(), is(locationA));
+        assertThat(proxy.source(), is(sourceName));
+        assertThat(proxy.workspaceName(), is(workspaceName));
+    }
+}

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjectorTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,84 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.jboss.dna.graph.Location;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class OffsetMirrorProjectorTest extends AbstractProjectorTest<OffsetMirrorProjector> {
+
+    private String mirrorSourceName;
+    private String mirrorWorkspaceName;
+
+    @Before
+    @Override
+    public void beforeEach() {
+        super.beforeEach();
+        this.mirrorSourceName = "source1";
+        this.mirrorWorkspaceName = "workspace1";
+        addProjection(mirrorSourceName, mirrorWorkspaceName, "/a/b/c => /");
+        this.projector = OffsetMirrorProjector.with(context, projections);
+    }
+
+    protected void assertProjectedIntoMirror( String federatedPath,
+                                              String pathInSource ) {
+        Location location = location(federatedPath);
+        ProjectedNode node = projector.project(context, location, false);
+        assertThat(node.isProxy(), is(true));
+        ProxyNode proxy = node.asProxy();
+        assertThat(proxy.location().getPath(), is(path(pathInSource)));
+        assertThat(proxy.source(), is(mirrorSourceName));
+        assertThat(proxy.workspaceName(), is(mirrorWorkspaceName));
+        assertThat(proxy.hasNext(), is(false));
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationAboveMirrorSource() {
+        assertPlacholderHasChildren("/", "a");
+        assertPlacholderHasChildren("/a", "b");
+        assertPlacholderHasChildren("/a/b", "c");
+        assertProjectedIntoMirror("/a/b/c", "/");
+    }
+
+    @Test
+    public void shouldAlwaysReturnProxyNodeForLocationWithinMirror() {
+        assertProjectedIntoMirror("/a/b/c", "/");
+        assertProjectedIntoMirror("/a/b/c/d", "/d");
+        assertProjectedIntoMirror("/a/b/c/d/e", "/d/e");
+    }
+
+    @Test
+    public void shouldReturnNoProjectedNodeForLocationOffPathToSourceBranch() {
+        assertNoProjectedNodeAt("/a[2]");
+        assertNoProjectedNodeAt("/a/b[2]");
+        assertNoProjectedNodeAt("/a/b/c[2]");
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/OffsetMirrorProjectorTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Copied: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionParserTest.java (from rev 910, trunk/dna-graph/src/main/java/org/jboss/dna/graph/DnaLexicon.java)
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionParserTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionParserTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,43 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class ProjectionParserTest {
+
+    private ProjectionParser parser;
+
+    @Test
+    public void shouldInitializeSingletonWithParserMethods() {
+        parser = ProjectionParser.getInstance();
+        assertThat(parser.getParserMethods().size(), is(1));
+    }
+
+}

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionPathRuleTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionPathRuleTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionPathRuleTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,250 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.matchers.JUnitMatchers.hasItems;
+import org.jboss.dna.common.text.TextEncoder;
+import org.jboss.dna.common.text.UrlEncoder;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class ProjectionPathRuleTest {
+
+    private ExecutionContext context;
+    private Projection.PathRule rule;
+    private PathFactory pathFactory;
+    private Path repositoryPath;
+    private Path sourcePath;
+    private Path[] validExceptions;
+    private NamespaceRegistry registry;
+    private TextEncoder encoder;
+
+    @Before
+    public void beforeEach() {
+        context = new ExecutionContext();
+        pathFactory = context.getValueFactories().getPathFactory();
+        registry = context.getNamespaceRegistry();
+        encoder = new UrlEncoder();
+        repositoryPath = pathFactory.create("/a/b/c");
+        sourcePath = pathFactory.create("/x/y");
+        validExceptions = new Path[] {pathFactory.create("e/f"), pathFactory.create("e/g")};
+        rule = new Projection.PathRule(repositoryPath, sourcePath, validExceptions);
+    }
+
+    @Test
+    public void shouldCreateInstanceWithValidRepositoryPathAndValidSourcePathAndNoExceptions() {
+        rule = new Projection.PathRule(repositoryPath, sourcePath);
+        assertThat(rule.getPathInRepository(), is(sameInstance(repositoryPath)));
+        assertThat(rule.getPathInSource(), is(sameInstance(sourcePath)));
+        assertThat(rule.hasExceptionsToRule(), is(false));
+    }
+
+    @Test
+    public void shouldCreateInstanceWithValidRepositoryPathAndValidSourcePathAndValidExceptions() {
+        rule = new Projection.PathRule(repositoryPath, sourcePath, validExceptions);
+        assertThat(rule.getPathInRepository(), is(sameInstance(repositoryPath)));
+        assertThat(rule.getPathInSource(), is(sameInstance(sourcePath)));
+        assertThat(rule.hasExceptionsToRule(), is(true));
+        assertThat(rule.getExceptionsToRule(), hasItems(validExceptions));
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailToCreateInstanceWithNullRepositoryPathAndValidSourcePathAndNoExceptions() {
+        repositoryPath = null;
+        new Projection.PathRule(repositoryPath, sourcePath);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailToCreateInstanceWithValidRepositoryPathAndNullSourcePathAndNoExceptions() {
+        sourcePath = null;
+        new Projection.PathRule(repositoryPath, sourcePath);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailToCreateInstanceWithValidRepositoryPathAndValidSourcePathAndAbsoluteExceptions() {
+        Path relativePath = validExceptions[0];
+        Path absolutePath = pathFactory.create("/j/k/l/m");
+        new Projection.PathRule(repositoryPath, sourcePath, relativePath, absolutePath);
+    }
+
+    @Test
+    public void shouldIncludeRepositoryPathsAtPathInRepository() {
+        assertThat(rule.includes(sourcePath), is(true));
+    }
+
+    @Test
+    public void shouldIncludeRepositoryPathsBelowPathInRepositoryThatAreNotExcluded() {
+        assertThat(rule.includes(pathFactory.create(sourcePath, "m")), is(true));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "m/n")), is(true));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "o/p")), is(true));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e/e")), is(true));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e")), is(true));
+    }
+
+    @Test
+    public void shouldNotIncludeRepositoryPathsBelowPathInRepositoryThatAreExcluded() {
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e/f")), is(false));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e/g")), is(false));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e/f/g")), is(false));
+        assertThat(rule.includes(pathFactory.create(sourcePath, "e/g/h")), is(false));
+    }
+
+    @Test
+    public void shouldNotIncludeRepositoryPathsNotBelowPathInRepository() {
+        assertThat(rule.includes(pathFactory.create("/m/n")), is(false));
+        assertThat(rule.includes(pathFactory.create("/x/y[3]")), is(false));
+    }
+
+    @Test
+    public void shouldProjectRepositoryPathIntoSourcePath() {
+        assertThat(rule.projectPathInRepositoryToPathInSource(repositoryPath, pathFactory), is(sourcePath));
+    }
+
+    @Test
+    public void shouldProjectPathBelowRepositoryPathIntoPathBelowSourcePath() {
+        Path pathInRepository = pathFactory.create(repositoryPath, "m/n");
+        Path pathInSource = pathFactory.create(sourcePath, "m/n");
+        assertThat(rule.projectPathInRepositoryToPathInSource(pathInRepository, pathFactory), is(pathInSource));
+
+        pathInRepository = pathFactory.create(repositoryPath, "m");
+        pathInSource = pathFactory.create(sourcePath, "m");
+        assertThat(rule.projectPathInRepositoryToPathInSource(pathInRepository, pathFactory), is(pathInSource));
+
+        pathInRepository = pathFactory.create(repositoryPath, "m/n[3]");
+        pathInSource = pathFactory.create(sourcePath, "m/n[3]");
+        assertThat(rule.projectPathInRepositoryToPathInSource(pathInRepository, pathFactory), is(pathInSource));
+    }
+
+    @Test
+    public void shouldProjectSourcePathIntoRepositoryPath() {
+        assertThat(rule.projectPathInSourceToPathInRepository(sourcePath, pathFactory), is(repositoryPath));
+    }
+
+    @Test
+    public void shouldProjectPathBelowSourcePathIntoPathBelowRepositoryPath() {
+        Path pathInRepository = pathFactory.create(repositoryPath, "m/n");
+        Path pathInSource = pathFactory.create(sourcePath, "m/n");
+        assertThat(rule.projectPathInSourceToPathInRepository(pathInSource, pathFactory), is(pathInRepository));
+
+        pathInRepository = pathFactory.create(repositoryPath, "m");
+        pathInSource = pathFactory.create(sourcePath, "m");
+        assertThat(rule.projectPathInSourceToPathInRepository(pathInSource, pathFactory), is(pathInRepository));
+
+        pathInRepository = pathFactory.create(repositoryPath, "m/n[3]");
+        pathInSource = pathFactory.create(sourcePath, "m/n[3]");
+        assertThat(rule.projectPathInSourceToPathInRepository(pathInSource, pathFactory), is(pathInRepository));
+    }
+
+    @Test
+    public void shouldGetPathsInRepositoryGivenPathsInSourceAtOrBelowSourcePathIfNotExcluded() {
+        assertThat(rule.getPathInRepository(sourcePath, pathFactory), is(repositoryPath));
+        assertThatGetPathInRepositoryReturnsCorrectPathInSource("");
+        assertThatGetPathInRepositoryReturnsCorrectPathInSource("m/n");
+        assertThatGetPathInRepositoryReturnsCorrectPathInSource("m[1]");
+        assertThatGetPathInRepositoryReturnsCorrectPathInSource("m[1]/n/o/p");
+    }
+
+    protected void assertThatGetPathInRepositoryReturnsCorrectPathInSource( String subpath ) {
+        assertThat(rule.getPathInRepository(pathFactory.create(sourcePath, subpath), pathFactory),
+                   is(pathFactory.create(repositoryPath, subpath)));
+    }
+
+    @Test
+    public void shouldGetNullPathInRepositoryGivenPathsInSourceAtOrBelowSourcePathIfExcluded() {
+        assertThat(rule.getPathInRepository(pathFactory.create(sourcePath, "e/f"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInRepository(pathFactory.create(sourcePath, "e/g"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInRepository(pathFactory.create(sourcePath, "e/f/h"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInRepository(pathFactory.create(sourcePath, "e/g/h"), pathFactory), is(nullValue()));
+    }
+
+    @Test
+    public void shouldGetNullPathInRepositoryGivenPathsInRepositoryNotAtOrBelowSourcePath() {
+        assertThat(rule.getPathInRepository(pathFactory.create("/m/n"), pathFactory), is(nullValue()));
+    }
+
+    @Test
+    public void shouldGetPathsInSourceGivenPathsInRepositoryAtOrBelowRepositoryPathIfNotExcluded() {
+        assertThat(rule.getPathInSource(repositoryPath, pathFactory), is(sourcePath));
+        assertThatGetPathInSourceReturnsCorrectPathInRepository("");
+        assertThatGetPathInSourceReturnsCorrectPathInRepository("m/n");
+        assertThatGetPathInSourceReturnsCorrectPathInRepository("m[1]");
+        assertThatGetPathInSourceReturnsCorrectPathInRepository("m[1]/n/o/p");
+
+    }
+
+    protected void assertThatGetPathInSourceReturnsCorrectPathInRepository( String subpath ) {
+        assertThat(rule.getPathInSource(pathFactory.create(repositoryPath, subpath), pathFactory),
+                   is(pathFactory.create(sourcePath, subpath)));
+    }
+
+    @Test
+    public void shouldGetNullPathInSourceGivenPathsInRepositoryAtOrBelowRepositoryPathIfExcluded() {
+        assertThat(rule.getPathInSource(pathFactory.create(repositoryPath, "e/f"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInSource(pathFactory.create(repositoryPath, "e/g"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInSource(pathFactory.create(repositoryPath, "e/f/h"), pathFactory), is(nullValue()));
+        assertThat(rule.getPathInSource(pathFactory.create(repositoryPath, "e/g/h"), pathFactory), is(nullValue()));
+    }
+
+    @Test
+    public void shouldGetNullPathInSourceGivenPathsInRepositoryNotAtOrBelowRepositoryPath() {
+        assertThat(rule.getPathInSource(pathFactory.create("/m/n"), pathFactory), is(nullValue()));
+    }
+
+    @Test
+    public void shouldConvertToString() {
+        assertThat(rule.getString(registry, encoder), is("/a/b/c => /x/y $ e/f $ e/g"));
+
+        repositoryPath = pathFactory.create("/a/b/c");
+        sourcePath = pathFactory.create("/");
+        rule = new Projection.PathRule(repositoryPath, sourcePath, validExceptions);
+        assertThat(rule.getString(registry, encoder), is("/a/b/c => / $ e/f $ e/g"));
+
+        repositoryPath = pathFactory.create("/");
+        sourcePath = pathFactory.create("/");
+        rule = new Projection.PathRule(repositoryPath, sourcePath, validExceptions);
+        assertThat(rule.getString(registry, encoder), is("/ => / $ e/f $ e/g"));
+    }
+
+    @Test
+    public void shouldHaveToString() {
+        assertThat(rule.toString(), is("/{}a/{}b/{}c => /{}x/{}y $ {}e/{}f $ {}e/{}g"));
+
+        repositoryPath = pathFactory.create("/a/b/c");
+        sourcePath = pathFactory.create("/");
+        rule = new Projection.PathRule(repositoryPath, sourcePath, validExceptions);
+        assertThat(rule.toString(), is("/{}a/{}b/{}c => / $ {}e/{}f $ {}e/{}g"));
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionPathRuleTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,290 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.matchers.JUnitMatchers.hasItems;
+import static org.mockito.Mockito.stub;
+import java.util.Set;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class ProjectionTest {
+
+    private ExecutionContext context;
+    private String sourceName;
+    private String workspaceName;
+    private Projection.Rule[] rules;
+    private Projection projection;
+    private PathFactory pathFactory;
+    private boolean readOnly;
+    @Mock
+    private Projection.Rule mockRule1;
+    @Mock
+    private Projection.Rule mockRule2;
+    @Mock
+    private Projection.Rule mockRule3;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        context = new ExecutionContext();
+        pathFactory = context.getValueFactories().getPathFactory();
+        sourceName = "Valid name";
+        workspaceName = "Valid workspace";
+        readOnly = false;
+        rules = new Projection.Rule[] {mockRule1, mockRule2, mockRule3};
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test
+    public void shouldCreateInstanceWithValidNameAndValidRules() {
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+        assertThat(projection.getSourceName(), is(sourceName));
+        assertThat(projection.getWorkspaceName(), is(workspaceName));
+        assertThat(projection.getRules().size(), is(rules.length));
+        assertThat(projection.getRules(), hasItems(mockRule1, mockRule2, mockRule3));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithNullNameAndValidRules() {
+        sourceName = null;
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithNullWorkspaceNameAndValidSourceNameAndRules() {
+        sourceName = null;
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithEmptyNameAndValidRules() {
+        sourceName = "";
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithBlankNameAndValidRules() {
+        sourceName = "   \t ";
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithValidNameAndNullRules() {
+        rules = null;
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithValidNameAndEmptyRules() {
+        rules = new Projection.Rule[] {};
+        projection = new Projection(sourceName, workspaceName, readOnly, rules);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateInstanceWithValidNameAndRulesArrayContainingAllNulls() {
+        projection = new Projection(sourceName, null, readOnly, null, null);
+    }
+
+    @Test
+    public void shouldCreateInstanceWithValidNameAndRulesAndShouldPruneNullRuleReferences() {
+        projection = new Projection(sourceName, workspaceName, readOnly, mockRule1, null, mockRule3);
+        assertThat(projection.getRules().size(), is(2));
+        assertThat(projection.getRules(), hasItems(mockRule1, mockRule3));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToGetPathsInSourceGivenValidPathAndNullPathFactory() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        projection.getPathsInSource(pathInRepository, null);
+    }
+
+    @Test
+    public void shouldGetNoPathsInSourceGivenNullPathInRepository() {
+        Set<Path> pathsInSource = projection.getPathsInSource(null, pathFactory);
+        assertThat(pathsInSource.isEmpty(), is(true));
+    }
+
+    @Test
+    public void shouldGetNoPathsInSourceGivenPathInRepositoryAndNoApplicableRules() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        stub(mockRule1.getPathInSource(pathInRepository, pathFactory)).toReturn(null);
+        stub(mockRule2.getPathInSource(pathInRepository, pathFactory)).toReturn(null);
+        stub(mockRule3.getPathInSource(pathInRepository, pathFactory)).toReturn(null);
+        Set<Path> pathsInSource = projection.getPathsInSource(pathInRepository, pathFactory);
+        assertThat(pathsInSource.isEmpty(), is(true));
+    }
+
+    @Test
+    public void shouldGetPathInSourceGivenPathInRepositoryAndOneApplicableRules() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        Path pathInSource = pathFactory.create("/d/e/f");
+        stub(mockRule1.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource);
+        stub(mockRule2.getPathInSource(pathInRepository, pathFactory)).toReturn(null);
+        stub(mockRule3.getPathInSource(pathInRepository, pathFactory)).toReturn(null);
+        Set<Path> pathsInSource = projection.getPathsInSource(pathInRepository, pathFactory);
+        assertThat(pathsInSource, hasItems(pathInSource));
+    }
+
+    @Test
+    public void shouldGetPathsInSourceGivenPathInRepositoryAndMultipleApplicableRules() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        Path pathInSource1 = pathFactory.create("/d/e/f");
+        Path pathInSource2 = pathFactory.create("/d/e/g");
+        Path pathInSource3 = pathFactory.create("/d/e/h");
+        stub(mockRule1.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource1);
+        stub(mockRule2.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource2);
+        stub(mockRule3.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource3);
+        Set<Path> pathsInSource = projection.getPathsInSource(pathInRepository, pathFactory);
+        assertThat(pathsInSource, hasItems(pathInSource1, pathInSource2, pathInSource3));
+    }
+
+    @Test
+    public void shouldGetPathsInSourceGivenPathInRepositoryAndMultipleApplicableRulesReturningDuplicatePathsInSource() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        Path pathInSource1 = pathFactory.create("/d/e/f");
+        Path pathInSource23 = pathFactory.create("/d/e/g");
+        stub(mockRule1.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource1);
+        stub(mockRule2.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource23);
+        stub(mockRule3.getPathInSource(pathInRepository, pathFactory)).toReturn(pathInSource23);
+        Set<Path> pathsInSource = projection.getPathsInSource(pathInRepository, pathFactory);
+        assertThat(pathsInSource, hasItems(pathInSource1, pathInSource23));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToGetPathsInRepositoryGivenValidPathAndNullPathFactory() {
+        Path pathInSource = pathFactory.create("/a/b/c");
+        projection.getPathsInRepository(pathInSource, null);
+    }
+
+    @Test
+    public void shouldGetNoPathsInRepositoryGivenNullPathInSource() {
+        Set<Path> pathsInRepository = projection.getPathsInRepository(null, pathFactory);
+        assertThat(pathsInRepository.isEmpty(), is(true));
+    }
+
+    @Test
+    public void shouldGetNoPathsInRepositoryGivenPathInSourceAndNoApplicableRules() {
+        Path pathInSource = pathFactory.create("/d/e/f");
+        stub(mockRule1.getPathInRepository(pathInSource, pathFactory)).toReturn(null);
+        stub(mockRule2.getPathInRepository(pathInSource, pathFactory)).toReturn(null);
+        stub(mockRule3.getPathInRepository(pathInSource, pathFactory)).toReturn(null);
+        Set<Path> pathsInRepository = projection.getPathsInRepository(pathInSource, pathFactory);
+        assertThat(pathsInRepository.isEmpty(), is(true));
+    }
+
+    @Test
+    public void shouldGetPathInRepositoryGivenPathInSourceAndOneApplicableRules() {
+        Path pathInRepository = pathFactory.create("/a/b/c");
+        Path pathInSource = pathFactory.create("/d/e/f");
+        stub(mockRule1.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository);
+        stub(mockRule2.getPathInRepository(pathInSource, pathFactory)).toReturn(null);
+        stub(mockRule3.getPathInRepository(pathInSource, pathFactory)).toReturn(null);
+        Set<Path> pathsInRepository = projection.getPathsInRepository(pathInSource, pathFactory);
+        assertThat(pathsInRepository, hasItems(pathInRepository));
+    }
+
+    @Test
+    public void shouldGetPathsInRepositoryGivenPathInSourceAndMultipleApplicableRules() {
+        Path pathInSource = pathFactory.create("/a/b/c");
+        Path pathInRepository1 = pathFactory.create("/d/e/f");
+        Path pathInRepository2 = pathFactory.create("/d/e/g");
+        Path pathInRepository3 = pathFactory.create("/d/e/h");
+        stub(mockRule1.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository1);
+        stub(mockRule2.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository2);
+        stub(mockRule3.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository3);
+        Set<Path> pathsInRepository = projection.getPathsInRepository(pathInSource, pathFactory);
+        assertThat(pathsInRepository, hasItems(pathInRepository1, pathInRepository2, pathInRepository3));
+    }
+
+    @Test
+    public void shouldGetPathsInRepositoryGivenPathInSourceAndMultipleApplicableRulesReturningDuplicatePathsInRepository() {
+        Path pathInSource = pathFactory.create("/a/b/c");
+        Path pathInRepository1 = pathFactory.create("/d/e/f");
+        Path pathInRepository23 = pathFactory.create("/d/e/g");
+        stub(mockRule1.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository1);
+        stub(mockRule2.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository23);
+        stub(mockRule3.getPathInRepository(pathInSource, pathFactory)).toReturn(pathInRepository23);
+        Set<Path> pathsInRepository = projection.getPathsInRepository(pathInSource, pathFactory);
+        assertThat(pathsInRepository, hasItems(pathInRepository1, pathInRepository23));
+    }
+
+    @Test
+    public void shouldParsePathRuleFromDefinitionWithNonRootRepositoryPathAndNonRootSourcePath() {
+        Projection.Rule rule = Projection.parsePathRule("/a => /b", context);
+        assertThat(rule, is(instanceOf(Projection.PathRule.class)));
+        Projection.PathRule pathRule = (Projection.PathRule)rule;
+        assertThat(pathRule.getPathInRepository(), is(pathFactory.create("/a")));
+        assertThat(pathRule.getPathInSource(), is(pathFactory.create("/b")));
+    }
+
+    @Test
+    public void shouldParsePathRuleFromDefinitionWithRootRepositoryPathAndNonRootSourcePath() {
+        Projection.Rule rule = Projection.parsePathRule("/ => /b", context);
+        assertThat(rule, is(instanceOf(Projection.PathRule.class)));
+        Projection.PathRule pathRule = (Projection.PathRule)rule;
+        assertThat(pathRule.getPathInRepository(), is(pathFactory.createRootPath()));
+        assertThat(pathRule.getPathInSource(), is(pathFactory.create("/b")));
+    }
+
+    @Test
+    public void shouldParsePathRuleFromDefinitionWithNonRootRepositoryPathAndRootSourcePath() {
+        Projection.Rule rule = Projection.parsePathRule("/a => /", context);
+        assertThat(rule, is(instanceOf(Projection.PathRule.class)));
+        Projection.PathRule pathRule = (Projection.PathRule)rule;
+        assertThat(pathRule.getPathInRepository(), is(pathFactory.create("/a")));
+        assertThat(pathRule.getPathInSource(), is(pathFactory.createRootPath()));
+    }
+
+    @Test
+    public void shouldParsePathRuleFromDefinitionWithRootRepositoryPathAndRootSourcePath() {
+        Projection.Rule rule = Projection.parsePathRule("/ => /", context);
+        assertThat(rule, is(instanceOf(Projection.PathRule.class)));
+        Projection.PathRule pathRule = (Projection.PathRule)rule;
+        assertThat(pathRule.getPathInRepository(), is(pathFactory.createRootPath()));
+        assertThat(pathRule.getPathInSource(), is(pathFactory.createRootPath()));
+    }
+
+    @Test
+    public void shouldNotParsePathRuleFromDefinitionWithRootRepositoryPathAndNoSourcePath() {
+        assertThat(Projection.parsePathRule("/", context), is(nullValue()));
+    }
+
+    @Test
+    public void shouldNotParsePathRuleFromDefinitionWithNonRootRepositoryPathAndNoSourcePath() {
+        assertThat(Projection.parsePathRule("a/", context), is(nullValue()));
+    }
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectionTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Added: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholdersTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholdersTest.java	                        (rev 0)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholdersTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -0,0 +1,182 @@
+/*
+ * 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.connector.federation;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.connector.federation.Projection.Rule;
+import org.jboss.dna.graph.property.Path;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class ProjectorWithPlaceholdersTest {
+
+    protected ExecutionContext context;
+    protected List<Projection> projections;
+    protected Collection<PlaceholderNode> placeholders;
+
+    @Before
+    public void beforeEach() {
+        this.context = new ExecutionContext();
+        this.projections = new ArrayList<Projection>();
+        this.placeholders = new ArrayList<PlaceholderNode>();
+    }
+
+    protected Rule[] rule( String... rule ) {
+        Rule[] rules = new Rule[rule.length];
+        for (int i = 0; i != rule.length; ++i) {
+            rules[i] = Projection.fromString(rule[i], context);
+        }
+        return rules;
+    }
+
+    protected Path path( String path ) {
+        return context.getValueFactories().getPathFactory().create(path);
+    }
+
+    protected Path path( Path parent,
+                         String relativePath ) {
+        return context.getValueFactories().getPathFactory().create(parent, relativePath);
+    }
+
+    protected void addProjection( String sourceName,
+                                  String workspaceName,
+                                  String... rules ) {
+        projections.add(new Projection(sourceName, workspaceName, false, rule(rules)));
+    }
+
+    protected PlaceholderNode getPlaceholder( Path path ) {
+        for (PlaceholderNode placeholder : placeholders) {
+            if (placeholder.location().getPath().equals(path)) return placeholder;
+        }
+        return null;
+    }
+
+    protected void assertPlacholderHasChildren( String parent,
+                                                String... childSegments ) {
+        Path parentPath = path(parent);
+        ProjectedNode node = getPlaceholder(parentPath);
+        assertThat(node, is(notNullValue()));
+        assertThat(node.isPlaceholder(), is(true));
+        PlaceholderNode placeholder = node.asPlaceholder();
+        List<Location> locations = new ArrayList<Location>();
+        for (String childSegment : childSegments) {
+            Path childPath = path(parentPath, childSegment);
+            locations.add(Location.create(childPath));
+        }
+        assertThat(placeholder.children(), is(locations));
+    }
+
+    protected void assertNoPlacholder( String parent ) {
+        assertThat(getPlaceholder(path(parent)), is(nullValue()));
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithOneProjectionNotAtRoot() {
+        addProjection("source1", "workspace1", "/a/b => /c/d");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertPlacholderHasChildren("/", "a");
+        assertPlacholderHasChildren("/a", "b");
+        assertThat(placeholders.size(), is(2));
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithOneProjectionAtRoot() {
+        addProjection("source1", "workspace1", "/ => /");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertNoPlacholder("/");
+        assertThat(placeholders.size(), is(0));
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithMultipleProjectionsNotAtRoot() {
+        addProjection("source1", "workspace1", "/a/b => /a1/b1");
+        addProjection("source2", "workspace2", "/c/d => /c1/d1");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertPlacholderHasChildren("/", "a", "c");
+        assertPlacholderHasChildren("/a", "b");
+        assertPlacholderHasChildren("/c", "d");
+        assertThat(placeholders.size(), is(3));
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithMultipleProjectionsAtRoot() {
+        addProjection("source1", "workspace1", "/ => /");
+        addProjection("source2", "workspace2", "/ => /");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertNoPlacholder("/");
+        assertThat(placeholders.size(), is(0));
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithMirrorAndBranchProjections() {
+        addProjection("source1", "workspace1", "/ => /");
+        addProjection("source2", "workspace2", "/a => /a");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertPlacholderHasChildren("/", "a"); // placeholder for root contains only branch root
+        assertThat(placeholders.size(), is(1));
+        assertNoPlacholder("/a");
+        assertNoPlacholder("/b");
+        assertNoPlacholder("/c/d");
+        assertNoPlacholder("/a/e");
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithMirrorAndOffsetBranchProjections() {
+        addProjection("source1", "workspace1", "/ => /");
+        addProjection("source2", "workspace2", "/a/b/c => /a/b");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertPlacholderHasChildren("/", "a"); // placeholder for root contains only branch root
+        assertPlacholderHasChildren("/a", "b");
+        assertPlacholderHasChildren("/a/b", "c");
+        assertThat(placeholders.size(), is(3));
+        assertNoPlacholder("/x");
+        assertNoPlacholder("/y/z");
+        assertNoPlacholder("/a/b/c");
+    }
+
+    @Test
+    public void shouldLoadPlaceholderNodesForWorkspaceWithOffsetMirrorProjection() {
+        addProjection("source1", "workspace1", "/a/b/c => /");
+        ProjectorWithPlaceholders.loadPlaceholderNodes(context, projections, placeholders);
+        assertPlacholderHasChildren("/", "a"); // placeholder for root contains only branch root
+        assertPlacholderHasChildren("/a", "b");
+        assertPlacholderHasChildren("/a/b", "c");
+        assertThat(placeholders.size(), is(3));
+        assertNoPlacholder("/a[2]");
+        assertNoPlacholder("/a/b[2]");
+        assertNoPlacholder("/a/b/c[2]");
+    }
+
+}


Property changes on: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/federation/ProjectorWithPlaceholdersTest.java
___________________________________________________________________
Name: svn:keywords
   + Id Revision
Name: svn:eol-style
   + LF

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryConnectorReadableTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryConnectorReadableTest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryConnectorReadableTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -79,7 +79,7 @@
         Location root = graph.getCurrentWorkspace().getRoot();
         Subgraph subgraph1 = graph.getSubgraphOfDepth(10).at(root);
         Subgraph subgraph2 = graph.getSubgraphOfDepth(10).at(root);
-        assertEquivalentSubgraphs(subgraph1, subgraph2, true);
+        assertEquivalentSubgraphs(subgraph1, subgraph2, true, true);
     }
 
 }

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/AbstractConnectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/AbstractConnectorTest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/AbstractConnectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -613,16 +613,22 @@
      * @param subgraph2 the second subgraph; may not be null
      * @param idPropertiesShouldMatch true if the identification properties of each corresponding node should match, or false if
      *        the identification properties should be ignored
+     * @param pathsShouldMatch true if the absolute paths of the subgraphs should match, or false if only the relative paths
+     *        within the subgraphs should match
      */
-    public void assertEquivalentSubgraphs( Subgraph subgraph1,
-                                           Subgraph subgraph2,
-                                           boolean idPropertiesShouldMatch ) {
+    public static void assertEquivalentSubgraphs( Subgraph subgraph1,
+                                                  Subgraph subgraph2,
+                                                  boolean idPropertiesShouldMatch,
+                                                  boolean pathsShouldMatch ) {
         assertThat(subgraph1, is(notNullValue()));
         assertThat(subgraph2, is(notNullValue()));
 
         // Shortcut ...
         if (subgraph1.getLocation().equals(subgraph2.getLocation())) return;
 
+        Path rootPath1 = subgraph1.getRoot().getLocation().getPath();
+        Path rootPath2 = subgraph2.getRoot().getLocation().getPath();
+
         // Iterate over each subgraph. Note that because each location should have a path, the path can be used
         // to ensure the structure matches.
         Iterator<SubgraphNode> iter1 = subgraph1.iterator();
@@ -631,10 +637,19 @@
             Node node1 = iter1.next();
             Node node2 = iter2.next();
 
+            assertThat(node1, is(notNullValue()));
+            assertThat(node2, is(notNullValue()));
+
             // Each node should have equivalent paths ..
             assertThat(node1.getLocation().hasPath(), is(true));
             assertThat(node2.getLocation().hasPath(), is(true));
-            assertThat(node1.getLocation().getPath(), is(node2.getLocation().getPath()));
+            if (pathsShouldMatch) {
+                assertThat(node1.getLocation().getPath(), is(node2.getLocation().getPath()));
+            } else {
+                Path relativeNode1 = node1.getLocation().getPath().relativeTo(rootPath1);
+                Path relativeNode2 = node2.getLocation().getPath().relativeTo(rootPath2);
+                assertThat(relativeNode1, is(relativeNode2));
+            }
 
             // Each node should have the same Identification properties ...
             if (idPropertiesShouldMatch) {
@@ -654,7 +669,7 @@
                     properties2.remove(idProperty.getName());
                 }
             }
-            assertThat(properties1.values(), is(properties2.values()));
+            assertThat(properties1, is(properties2));
 
             // Each node should have the same children. We can check this, tho this will be enforced when comparing paths ...
             assertThat(node1.getChildrenSegments(), is(node2.getChildrenSegments()));
@@ -672,8 +687,8 @@
      * @param node1 the first node; may not be null
      * @param node2 the second node; may not be null
      */
-    public void assertSameNode( Node node1,
-                                Node node2 ) {
+    public static void assertSameNode( Node node1,
+                                       Node node2 ) {
         assertThat(node1, is(notNullValue()));
         assertThat(node2, is(notNullValue()));
 
@@ -701,8 +716,8 @@
      * @param node the node; may not be null
      * @param properties the expected properties
      */
-    public void assertSameProperties( Node node,
-                                      Map<Name, Property> properties ) {
+    public static void assertSameProperties( Node node,
+                                             Map<Name, Property> properties ) {
         assertThat(node, is(notNullValue()));
         assertThat(properties, is(notNullValue()));
         Set<Name> names = new HashSet<Name>(properties.keySet());
@@ -720,8 +735,8 @@
      * @param node the node; may not be null
      * @param properties the expected properties
      */
-    public void assertSameProperties( Node node,
-                                      Iterable<Property> properties ) {
+    public static void assertSameProperties( Node node,
+                                             Iterable<Property> properties ) {
         assertThat(node, is(notNullValue()));
         assertThat(properties, is(notNullValue()));
         Set<Name> names = new HashSet<Name>(node.getPropertiesByName().keySet());

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/ReadableConnectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/ReadableConnectorTest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/ReadableConnectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -305,7 +305,7 @@
         Subgraph subgraph1 = graph.getSubgraphOfDepth(10).at(root);
         for (int i = 0; i != 4; ++i) {
             Subgraph subgraph2 = graph.getSubgraphOfDepth(10).at(root);
-            assertEquivalentSubgraphs(subgraph1, subgraph2, true);
+            assertEquivalentSubgraphs(subgraph1, subgraph2, true, true);
         }
     }
 }

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WorkspaceConnectorTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WorkspaceConnectorTest.java	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WorkspaceConnectorTest.java	2009-05-18 15:36:58 UTC (rev 911)
@@ -283,7 +283,7 @@
                 // Verify that the content of 'workspace1' matches that of 'workspace2'
                 Subgraph subgraph1 = graph.getSubgraphOfDepth(100000).at(workspace1.getRoot());
                 Subgraph subgraph2 = graph.getSubgraphOfDepth(100000).at(workspace2.getRoot());
-                assertEquivalentSubgraphs(subgraph1, subgraph2, true);
+                assertEquivalentSubgraphs(subgraph1, subgraph2, true, true);
             } catch (InvalidRequestException error) {
                 // Updates may not be supported, but if they are then this is a failure ...
                 if (source.getCapabilities().supportsUpdates()) throw error;

Modified: trunk/pom.xml
===================================================================
--- trunk/pom.xml	2009-05-15 19:28:48 UTC (rev 910)
+++ trunk/pom.xml	2009-05-18 15:36:58 UTC (rev 911)
@@ -144,7 +144,7 @@
 		<module>extensions/dna-common-jdbc</module> 
 		<module>extensions/dna-connector-jdbc-metadata</module> 
 		<module>dna-integration-tests</module>
-		<module>docs/examples/gettingstarted</module>
+		<!--module>docs/examples/gettingstarted</module-->
 	</modules>
 
 	<profiles>




More information about the dna-commits mailing list