[dna-commits] DNA SVN: r440 - in trunk: dna-spi/src/main/java/org/jboss/dna/spi/cache and 11 other directories.

dna-commits at lists.jboss.org dna-commits at lists.jboss.org
Mon Aug 18 17:38:59 EDT 2008


Author: rhauch
Date: 2008-08-18 17:38:58 -0400 (Mon, 18 Aug 2008)
New Revision: 440

Added:
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FederatedNodeTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlanTest.java
   trunk/extensions/dna-connector-federation/src/test/resources/log4j.properties
Removed:
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/BasicFederatedNodeTest.java
Modified:
   trunk/dna-repository/src/test/java/org/jboss/dna/repository/RepositoryServiceTest.java
   trunk/dna-spi/src/main/java/org/jboss/dna/spi/cache/BasicCachePolicy.java
   trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepository.java
   trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepositorySource.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatedRepositorySource.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederationI18n.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/Projection.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/Contribution.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/EmptyContribution.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutor.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/StandardMergeStrategy.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlan.java
   trunk/extensions/dna-connector-federation/src/main/resources/org/jboss/dna/connector/federation/FederationI18n.properties
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/FederatedRepositorySourceTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/ProjectionTest.java
   trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutorTest.java
   trunk/extensions/dna-connector-jbosscache/src/test/java/org/jboss/dna/connector/jbosscache/JBossCacheSourceTest.java
Log:
DNA-188 - Complete the federating command executor
http://jira.jboss.com/jira/browse/DNA-188

Changed the signature of the BasicCachePolicy, which affected several tests and test classes.  Also added quite a few more unit tests to the federation connector code.

Modified: trunk/dna-repository/src/test/java/org/jboss/dna/repository/RepositoryServiceTest.java
===================================================================
--- trunk/dna-repository/src/test/java/org/jboss/dna/repository/RepositoryServiceTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/dna-repository/src/test/java/org/jboss/dna/repository/RepositoryServiceTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -96,7 +96,7 @@
         configSourceName = "configSource";
         Projection.Rule configProjectionRule = new Projection.PathRule(pathInRepository, pathInSource);
         configProjection = new Projection(configSourceName, configProjectionRule);
-        configRepository = new SimpleRepository("Configuration Repository");
+        configRepository = SimpleRepository.get("Configuration Repository");
         configRepositorySource = new SimpleRepositorySource();
         configRepositorySource.setRepositoryName(configRepository.getRepositoryName());
         configRepositorySource.setName(configSourceName);

Modified: trunk/dna-spi/src/main/java/org/jboss/dna/spi/cache/BasicCachePolicy.java
===================================================================
--- trunk/dna-spi/src/main/java/org/jboss/dna/spi/cache/BasicCachePolicy.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/dna-spi/src/main/java/org/jboss/dna/spi/cache/BasicCachePolicy.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -21,19 +21,24 @@
  */
 package org.jboss.dna.spi.cache;
 
+import java.util.concurrent.TimeUnit;
+import org.jboss.dna.common.util.ArgCheck;
+
 /**
  * @author Randall Hauch
  */
 public class BasicCachePolicy implements CachePolicy {
 
     private static final long serialVersionUID = 1L;
-    private long timeToLive;
+    private long timeToLiveInMillis = 0L;
 
     public BasicCachePolicy() {
     }
 
-    public BasicCachePolicy( long timeToCache ) {
-        this.timeToLive = timeToCache;
+    public BasicCachePolicy( long timeToCache,
+                             TimeUnit unit ) {
+        ArgCheck.isNotNull(unit, "unit");
+        this.timeToLiveInMillis = TimeUnit.MILLISECONDS.convert(timeToCache, unit);
     }
 
     /**
@@ -42,18 +47,22 @@
      * @see org.jboss.dna.spi.cache.CachePolicy#getTimeToLive()
      */
     public long getTimeToLive() {
-        return this.timeToLive;
+        return this.timeToLiveInMillis;
     }
 
     /**
+     * Set the time for values and information to live in the cache.
+     * 
      * @param timeToLive Sets timeToLive to the specified value.
+     * @param unit the unit in which the time to live value is defined
      */
-    public void setTimeToLive( long timeToLive ) {
-        this.timeToLive = timeToLive;
+    public void setTimeToLive( long timeToLive,
+                               TimeUnit unit ) {
+        this.timeToLiveInMillis = TimeUnit.NANOSECONDS.convert(timeToLive, unit);
     }
 
     public boolean isEmpty() {
-        return this.timeToLive == 0;
+        return this.timeToLiveInMillis == 0;
     }
 
     public CachePolicy getUnmodifiable() {
@@ -83,7 +92,7 @@
      */
     @Override
     public String toString() {
-        return "{ TTL=" + this.timeToLive + " }";
+        return "{ TTL=" + this.timeToLiveInMillis + " ms }";
     }
 
 }

Modified: trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepository.java
===================================================================
--- trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepository.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepository.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -37,6 +37,7 @@
 import org.jboss.dna.spi.graph.PathFactory;
 import org.jboss.dna.spi.graph.Property;
 import org.jboss.dna.spi.graph.PropertyFactory;
+import org.jboss.dna.spi.graph.impl.BasicPath;
 
 /**
  * A very simple repository that maintains properties for nodes identified by a path, and computes the children based upon the set
@@ -57,7 +58,9 @@
     private static final ConcurrentMap<String, SimpleRepository> repositoriesByName = new ConcurrentHashMap<String, SimpleRepository>();
 
     public static SimpleRepository get( String name ) {
-        return repositoriesByName.get(name);
+        SimpleRepository newRepository = new SimpleRepository(name);
+        SimpleRepository repository = repositoriesByName.putIfAbsent(name, newRepository);
+        return repository != null ? repository : newRepository;
     }
 
     public static void shutdownAll() {
@@ -73,9 +76,11 @@
 
     public SimpleRepository( String repositoryName ) {
         this.repositoryName = repositoryName;
-        if (repositoriesByName.putIfAbsent(repositoryName, this) != null) {
-            throw new IllegalArgumentException("Repository \"" + repositoryName + "\" already exists and may not be recreated");
-        }
+        // if (repositoriesByName.putIfAbsent(repositoryName, this) != null) {
+        // throw new IllegalArgumentException("Repository \"" + repositoryName + "\" already exists and may not be recreated");
+        // }
+        // Create a root node ...
+        data.putIfAbsent(BasicPath.ROOT, new HashMap<Name, Property>());
     }
 
     /**

Modified: trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepositorySource.java
===================================================================
--- trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepositorySource.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/dna-spi/src/test/java/org/jboss/dna/spi/connector/SimpleRepositorySource.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -59,6 +59,7 @@
     private String repositoryName;
     private String name;
     private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
+    private CachePolicy defaultCachePolicy;
 
     public SimpleRepositorySource() {
         super();
@@ -113,6 +114,20 @@
     }
 
     /**
+     * @return defaultCachePolicy
+     */
+    public CachePolicy getDefaultCachePolicy() {
+        return defaultCachePolicy;
+    }
+
+    /**
+     * @param defaultCachePolicy Sets defaultCachePolicy to the specified value.
+     */
+    public void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) {
+        this.defaultCachePolicy = defaultCachePolicy;
+    }
+
+    /**
      * {@inheritDoc}
      * 
      * @see javax.naming.Referenceable#getReference()
@@ -174,21 +189,25 @@
      */
     public RepositoryConnection getConnection() throws RepositorySourceException {
         String reposName = this.getRepositoryName();
+        if (reposName == null) throw new RepositorySourceException("Invalid repository source: missing repository name");
         SimpleRepository repository = SimpleRepository.get(reposName);
         if (repository == null) {
             throw new RepositorySourceException(this.getName(), "Unable to find repository \"" + reposName + "\"");
         }
-        return new Connection(repository);
+        return new Connection(repository, this.getDefaultCachePolicy());
     }
 
     protected class Connection implements RepositoryConnection {
 
         private RepositorySourceListener listener;
         private final SimpleRepository repository;
+        private final CachePolicy defaultCachePolicy;
 
-        protected Connection( SimpleRepository repository ) {
+        protected Connection( SimpleRepository repository,
+                              CachePolicy defaultCachePolicy ) {
             assert repository != null;
             this.repository = repository;
+            this.defaultCachePolicy = defaultCachePolicy;
         }
 
         /**
@@ -225,7 +244,7 @@
          * @see org.jboss.dna.spi.connector.RepositoryConnection#getDefaultCachePolicy()
          */
         public CachePolicy getDefaultCachePolicy() {
-            return null;
+            return defaultCachePolicy;
         }
 
         /**

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatedRepositorySource.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatedRepositorySource.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatedRepositorySource.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -28,6 +28,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import javax.naming.Context;
 import javax.naming.InitialContext;
@@ -823,7 +824,7 @@
             BasicCachePolicy cachePolicy = new BasicCachePolicy();
             Property timeToLiveProperty = getRepository.getPropertiesByName().get(nameFactory.create(CACHE_POLICY_TIME_TO_LIVE_CONFIG_PROPERTY_NAME));
             if (timeToLiveProperty != null && !timeToLiveProperty.isEmpty()) {
-                cachePolicy.setTimeToLive(longFactory.create(timeToLiveProperty.getValues().next()));
+                cachePolicy.setTimeToLive(longFactory.create(timeToLiveProperty.getValues().next()), TimeUnit.MILLISECONDS);
             }
             CachePolicy defaultCachePolicy = cachePolicy.isEmpty() ? null : cachePolicy.getUnmodifiable();
             return new FederatedRepositoryConfig(repositoryName, cacheProjection, sourceProjections, defaultCachePolicy);

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederationI18n.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederationI18n.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederationI18n.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -32,6 +32,8 @@
 
     public static I18n requiredNodeDoesNotExistRelativeToNode;
     public static I18n propertyIsRequired;
+    public static I18n nodeDoesNotExistAtPath;
+    public static I18n errorRemovingNodeFromCache;
 
     public static I18n interruptedWhileUsingFederationConfigurationRepository;
     public static I18n unableToFindFederatedRepositoryInJndi;

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/Projection.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/Projection.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/Projection.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -172,7 +172,7 @@
      * Pattern that identifies the form:
      * 
      * <pre>
-     *    repository_path [=&gt; source_path [$ exception ]*]?
+     *    repository_path =&gt; source_path [$ exception ]*
      * </pre>
      * 
      * where the following groups are captured on the first call to {@link Matcher#find()}:
@@ -212,6 +212,10 @@
         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);
@@ -315,6 +319,21 @@
     }
 
     /**
+     * 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 set of top-level paths; never null
+     */
+    public Set<Path> getTopLevelPathsInRepository( PathFactory factory ) {
+        ArgCheck.isNotNull(factory, "factory");
+        Set<Path> paths = new HashSet<Path>();
+        for (Rule rule : getRules()) {
+            paths.addAll(rule.getTopLevelPathsInRepository(factory));
+        }
+        return paths;
+    }
+
+    /**
      * Determine whether this project is a simple projection that only involves for any one repository path no more than a single
      * source path.
      * 
@@ -415,7 +434,7 @@
     }
 
     /**
-     * A rule used within a project do define how content within a source is project into the federated repository. This mapping
+     * 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>
@@ -426,7 +445,16 @@
      */
     @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 set of top-level paths; never null
+         */
+        public abstract Set<Path> getTopLevelPathsInRepository( PathFactory factory );
+
+        /**
          * 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.
          * 
@@ -585,6 +613,16 @@
 
         /**
          * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.connector.federation.Projection.Rule#getTopLevelPathsInRepository(org.jboss.dna.spi.graph.PathFactory)
+         */
+        @Override
+        public Set<Path> getTopLevelPathsInRepository( PathFactory factory ) {
+            return Collections.singleton(getPathInRepository());
+        }
+
+        /**
+         * {@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.

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/Contribution.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/Contribution.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/Contribution.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -26,11 +26,13 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.StringUtil;
 import org.jboss.dna.spi.graph.DateTime;
 import org.jboss.dna.spi.graph.Name;
 import org.jboss.dna.spi.graph.Path;
 import org.jboss.dna.spi.graph.Property;
 import org.jboss.dna.spi.graph.Path.Segment;
+import org.jboss.dna.spi.graph.impl.JodaDateTime;
 
 /**
  * The contribution of a source to the information for a single federated node. Users of this interface should treat contributions
@@ -287,6 +289,15 @@
     }
 
     /**
+     * Return whether this contribution is an empty contribution.
+     * 
+     * @return true if this contribution is empty, or false otherwise
+     */
+    public boolean isEmpty() {
+        return false;
+    }
+
+    /**
      * {@inheritDoc}
      * <p>
      * This implementation returns the hash code of the {@link #getSourceName() source name}, and is compatible with the
@@ -300,6 +311,51 @@
 
     /**
      * {@inheritDoc}
+     * 
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append("Contribution from \"");
+        sb.append(getSourceName());
+        if (isExpired(new JodaDateTime().toUtcTimeZone())) {
+            sb.append("\": expired ");
+        } else {
+            sb.append("\": expires ");
+        }
+        sb.append(getExpirationTimeInUtc().getString());
+        if (getPropertyCount() != 0) {
+            sb.append(" { ");
+            boolean first = true;
+            Iterator<Property> propIter = getProperties();
+            while (propIter.hasNext()) {
+                if (!first) sb.append(", ");
+                else first = false;
+                Property property = propIter.next();
+                sb.append(property.getName());
+                sb.append('=');
+                sb.append(StringUtil.readableString(property.getValuesAsArray()));
+            }
+            sb.append(" }");
+        }
+        if (getChildrenCount() != 0) {
+            sb.append("< ");
+            boolean first = true;
+            Iterator<Segment> childIter = getChildren();
+            while (childIter.hasNext()) {
+                if (!first) sb.append(", ");
+                else first = false;
+                Segment child = childIter.next();
+                sb.append(child);
+            }
+            sb.append(" >");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * {@inheritDoc}
      * <p>
      * This implementation only compares the {@link #getSourceName() source name}.
      * </p>

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/EmptyContribution.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/EmptyContribution.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/contribution/EmptyContribution.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -68,8 +68,18 @@
 
     /**
      * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.connector.federation.contribution.Contribution#isEmpty()
      */
     @Override
+    public boolean isEmpty() {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public boolean equals( Object obj ) {
         if (obj == this) return true;
         if (obj instanceof EmptyContribution) {

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutor.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutor.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutor.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -30,8 +30,11 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import net.jcip.annotations.NotThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
 import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.connector.federation.FederationI18n;
 import org.jboss.dna.connector.federation.Projection;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 import org.jboss.dna.connector.federation.merge.FederatedNode;
@@ -47,7 +50,6 @@
 import org.jboss.dna.spi.connector.RepositorySource;
 import org.jboss.dna.spi.connector.RepositorySourceException;
 import org.jboss.dna.spi.graph.DateTime;
-import org.jboss.dna.spi.graph.DateTimeFactory;
 import org.jboss.dna.spi.graph.Name;
 import org.jboss.dna.spi.graph.Path;
 import org.jboss.dna.spi.graph.PathFactory;
@@ -61,7 +63,6 @@
 import org.jboss.dna.spi.graph.commands.NodeConflictBehavior;
 import org.jboss.dna.spi.graph.commands.executor.AbstractCommandExecutor;
 import org.jboss.dna.spi.graph.commands.impl.BasicCreateNodeCommand;
-import org.jboss.dna.spi.graph.commands.impl.BasicGetChildrenCommand;
 import org.jboss.dna.spi.graph.commands.impl.BasicGetNodeCommand;
 import org.jboss.dna.spi.graph.impl.BasicSingleValueProperty;
 
@@ -78,11 +79,12 @@
     private final List<Projection> sourceProjections;
     private final Set<String> sourceNames;
     private final RepositoryConnectionFactory connectionFactory;
-    private final MergeStrategy mergingStrategy;
+    private MergeStrategy mergingStrategy;
     /** The set of all connections, including the cache connection */
     private final Map<String, RepositoryConnection> connectionsBySourceName;
     /** A direct reference to the cache connection */
     private RepositoryConnection cacheConnection;
+    private Logger logger;
 
     /**
      * Create a command executor that federates (merges) the information from multiple sources described by the source
@@ -139,14 +141,52 @@
         for (Projection projection : this.sourceProjections) {
             this.sourceNames.add(projection.getSourceName());
         }
-        if (this.sourceProjections.size() == 1 && this.sourceProjections.get(0).isSimple()) {
-            this.mergingStrategy = new OneContributionMergeStrategy();
+        setLogger(null);
+        setMergingStrategy(null);
+    }
+
+    /**
+     * @param logger Sets logger to the specified value.
+     */
+    public void setLogger( Logger logger ) {
+        this.logger = logger != null ? logger : Logger.getLogger(getClass());
+    }
+
+    /**
+     * @param mergingStrategy Sets mergingStrategy to the specified value.
+     */
+    public void setMergingStrategy( MergeStrategy mergingStrategy ) {
+        if (mergingStrategy != null) {
+            this.mergingStrategy = mergingStrategy;
         } else {
-            this.mergingStrategy = new StandardMergeStrategy(DnaLexicon.UUID);
+            if (this.sourceProjections.size() == 1 && this.sourceProjections.get(0).isSimple()) {
+                this.mergingStrategy = new OneContributionMergeStrategy();
+            } else {
+                this.mergingStrategy = new StandardMergeStrategy(DnaLexicon.UUID);
+            }
         }
+        assert this.mergingStrategy != null;
     }
 
     /**
+     * Get an unmodifiable list of the immutable source projections.
+     * 
+     * @return the set of projections used as sources; never null
+     */
+    public List<Projection> getSourceProjections() {
+        return Collections.unmodifiableList(sourceProjections);
+    }
+
+    /**
+     * Get the projection defining the cache.
+     * 
+     * @return the cache projection
+     */
+    public Projection getCacheProjection() {
+        return cacheProjection;
+    }
+
+    /**
      * {@inheritDoc}
      * 
      * @see org.jboss.dna.spi.graph.commands.executor.AbstractCommandExecutor#close()
@@ -187,6 +227,10 @@
         return connection;
     }
 
+    protected Set<String> getOpenConnections() {
+        return connectionsBySourceName.keySet();
+    }
+
     /**
      * {@inheritDoc}
      * <p>
@@ -199,6 +243,7 @@
     @Override
     public void execute( GetNodeCommand command ) throws RepositorySourceException, InterruptedException {
         BasicGetNodeCommand nodeInfo = getNode(command.getPath());
+        if (nodeInfo.hasError()) return;
         for (Property property : nodeInfo.getProperties()) {
             command.setProperty(property);
         }
@@ -215,6 +260,7 @@
     @Override
     public void execute( GetPropertiesCommand command ) throws RepositorySourceException, InterruptedException {
         BasicGetNodeCommand nodeInfo = getNode(command.getPath());
+        if (nodeInfo.hasError()) return;
         for (Property property : nodeInfo.getProperties()) {
             command.setProperty(property);
         }
@@ -228,6 +274,7 @@
     @Override
     public void execute( GetChildrenCommand command ) throws RepositorySourceException, InterruptedException {
         BasicGetNodeCommand nodeInfo = getNode(command.getPath());
+        if (nodeInfo.hasError()) return;
         for (Segment child : nodeInfo.getChildren()) {
             command.addChild(child, nodeInfo.getChildIdentityProperties(child));
         }
@@ -247,106 +294,191 @@
         RepositoryConnection cacheConnection = getConnectionToCache();
         BasicGetNodeCommand fromCache = new BasicGetNodeCommand(path);
         cacheConnection.execute(context, fromCache);
+
+        // Look at the cache results from the cache for problems, or if found a plan in the cache look
+        // at the contributions. We'll be putting together the set of source names for which we need to
+        // get the contributions.
+        Set<String> sourceNames = null;
+        List<Contribution> contributions = new LinkedList<Contribution>();
+
         if (fromCache.hasError()) {
-            if (fromCache.getError() instanceof PathNotFoundException) {
-                // The path was not found in the cache, so since we don't know whether the ancestors are federated
-                // from multiple source nodes, we need to populate the cache starting with the lowest ancestor
-                // that already exists in the cache.
-                PathNotFoundException notFound = (PathNotFoundException)fromCache.getError();
-                Path lowestExistingAncestor = notFound.getLowestAncestorThatDoesExist();
-                Path ancestor = path.getAncestor();
+            Throwable error = fromCache.getError();
+            if (!(error instanceof PathNotFoundException)) return fromCache;
 
-                if (!ancestor.equals(lowestExistingAncestor)) {
-                    // Create the commands that load the children (not properties) of the existing ancestor,
-                    // then the children (not properties) of the child next on the desired path, etc.,
-                    // down to (but excluding) the desired path.
-                    LinkedList<BasicGetChildrenCommand> loadChildrenCommands = new LinkedList<BasicGetChildrenCommand>();
-                    Path pathToLoad = path.getAncestor();
-                    while (!pathToLoad.equals(lowestExistingAncestor)) {
-                        BasicGetChildrenCommand command = new BasicGetChildrenCommand(pathToLoad);
-                        loadChildrenCommands.addFirst(command);
-                        pathToLoad = pathToLoad.getAncestor();
+            // The path was not found in the cache, so since we don't know whether the ancestors are federated
+            // from multiple source nodes, we need to populate the cache starting with the lowest ancestor
+            // that already exists in the cache.
+            PathNotFoundException notFound = (PathNotFoundException)fromCache.getError();
+            Path lowestExistingAncestor = notFound.getLowestAncestorThatDoesExist();
+            Path ancestor = path.getAncestor();
+
+            if (!ancestor.equals(lowestExistingAncestor)) {
+                // Load the nodes along the path below the existing ancestor, down to (but excluding) the desired path
+                Path pathToLoad = path.getAncestor();
+                while (!pathToLoad.equals(lowestExistingAncestor)) {
+                    loadContributionsFromSources(pathToLoad, null, contributions); // sourceNames may be null or empty
+                    FederatedNode mergedNode = createFederatedNode(pathToLoad, contributions, true);
+                    if (mergedNode == null) {
+                        // No source had a contribution ...
+                        I18n msg = FederationI18n.nodeDoesNotExistAtPath;
+                        fromCache.setError(new PathNotFoundException(path, ancestor, msg.text(path, ancestor)));
+                        return fromCache;
                     }
-                    // Now execute these commands (one-by-one is fine, since this is an executor) ...
-                    for (BasicGetChildrenCommand command : loadChildrenCommands) {
-                        execute(command);
+                    contributions.clear();
+                    // Move to the next child along the path ...
+                    pathToLoad = pathToLoad.getAncestor();
+                }
+            }
+            // At this point, all ancestors exist ...
+        } else {
+            // There is no error, so look for the merge plan ...
+            MergePlan mergePlan = getMergePlan(fromCache);
+            if (mergePlan != null) {
+                // We found the merge plan, so check whether it's still valid ...
+                final DateTime now = getCurrentTimeInUtc();
+                if (mergePlan.isExpired(now)) {
+                    // It is still valid, so check whether any contribution is from a non-existant projection ...
+                    for (Contribution contribution : mergePlan) {
+                        if (!this.sourceNames.contains(contribution.getSourceName())) {
+                            // TODO: Record that the cached contribution is from a source that is no longer in this repository
+                        }
                     }
+                    return fromCache;
                 }
 
-                // Load the node from the sources ...
-                FederatedNode mergedNode = loadFromSources(path);
+                // At least one of the contributions is expired, so go through the contributions and place
+                // the valid contributions in the 'contributions' list; any expired contribution
+                // needs to be loaded by adding the name to the 'sourceNames'
+                if (mergePlan.getContributionCount() > 0) {
+                    sourceNames = new HashSet<String>(sourceNames);
+                    for (Contribution contribution : mergePlan) {
+                        if (!contribution.isExpired(now)) {
+                            sourceNames.remove(contribution.getSourceName());
+                            contributions.add(contribution);
+                        }
+                    }
+                }
+            }
+        }
 
-                // Place the results into the cache ...
-                updateCache(mergedNode);
+        // Get the contributions from the sources given their names ...
+        loadContributionsFromSources(path, sourceNames, contributions); // sourceNames may be null or empty
+        FederatedNode mergedNode = createFederatedNode(path, contributions, true);
+        if (mergedNode == null) {
+            // No source had a contribution ...
+            Path ancestor = path.getAncestor();
+            I18n msg = FederationI18n.nodeDoesNotExistAtPath;
+            fromCache.setError(new PathNotFoundException(path, ancestor, msg.text(path, ancestor)));
+            return fromCache;
+        }
+        return mergedNode;
+    }
 
-                // Return the results ...
-                return mergedNode;
+    protected FederatedNode createFederatedNode( Path path,
+                                                 List<Contribution> contributions,
+                                                 boolean updateCache ) throws RepositorySourceException, InterruptedException {
+
+        // If there are no contributions from any source ...
+        boolean foundNonEmptyContribution = false;
+        for (Contribution contribution : contributions) {
+            if (!contribution.isEmpty()) {
+                foundNonEmptyContribution = true;
+                break;
             }
-            return fromCache; // with an error
         }
+        if (foundNonEmptyContribution) return null;
+        if (logger.isTraceEnabled()) {
+            logger.trace("Loaded {0} from sources, resulting in these contributions:", path);
+            int i = 0;
+            for (Contribution contribution : contributions) {
+                logger.trace("  {0} {1}", ++i, contribution);
+            }
+        }
 
-        // The cache had the node information, so look for the merge plan ...
-        MergePlan mergePlan = getMergePlan(fromCache);
-        assert mergePlan != null;
-        if (!isCurrent(path, mergePlan)) {
-            // Some of the merge plan is out of date, so we need to read the information from those regions that are expired ...
-            // MergePlan newMergePlan = new BasicMergePlan();
-            // for (Projection projection : sourceProjections) {
-            // // Does this region apply to the path ...
-            // if (projection.appliesTo(path)) {
-            // // Get any existing contribution ...
-            // Contribution contribution = mergePlan.getContributionFrom(region.getSourceName());
-            // if (contribution == null || contribution.isExpired(getCurrentTimeInUtc())) {
-            // contribution = getContribution(contribution.getSourceName(), path);
-            // }
-            // newMergePlan.addContribution(contribution);
-            // }
-            // }
-            // for ( Contribution contribution : mergePlan.getContributions() ) {
-            // // Is the contribution still represented by a region?
-            //                
-            // if ( contribution.isExpired(getCurrentTimeInUtc())) {
-            // contribution = getContribution(contribution.getSourceName(),path);
-            // }
-            // newMergePlan.addContribution(contribution);
-            // }
-
-            // What about other (new) regions?
+        // Merge the results into a single set of results ...
+        FederatedNode mergedNode = new FederatedNode(path, UUID.randomUUID());
+        ExecutionContext context = getExecutionContext();
+        mergingStrategy.merge(mergedNode, contributions, context);
+        if (mergedNode.getCachePolicy() == null) {
+            mergedNode.setCachePolicy(defaultCachePolicy);
         }
-
-        return fromCache;
+        if (updateCache) {
+            // Place the results into the cache ...
+            updateCache(mergedNode);
+        }
+        // And return the results ...
+        return mergedNode;
     }
 
     /**
+     * Load the node at the supplied path from the sources with the supplied name, returning the information. This method always
+     * obtains the information from the sources and does not use or update the cache.
+     * 
      * @param path the path of the node that is to be loaded
-     * @return the federated node containing information loaded from the sources
+     * @param sourceNames the names of the sources from which contributions are to be loaded; may be empty or null if all
+     *        contributions from all sources are to be loaded
+     * @param contributions the list into which the contributions are to be placed
      * @throws InterruptedException
      * @throws RepositorySourceException
      */
-    protected FederatedNode loadFromSources( Path path ) throws RepositorySourceException, InterruptedException {
+    protected void loadContributionsFromSources( Path path,
+                                                 Set<String> sourceNames,
+                                                 List<Contribution> contributions )
+        throws RepositorySourceException, InterruptedException {
         // At this point, there is no merge plan, so read information from the sources ...
         ExecutionContext context = getExecutionContext();
         PathFactory pathFactory = context.getValueFactories().getPathFactory();
-        DateTimeFactory timeFactory = context.getValueFactories().getDateFactory();
-        List<Contribution> contributions = new LinkedList<Contribution>();
         for (Projection projection : this.sourceProjections) {
             final String source = projection.getSourceName();
+            if (sourceNames != null && !sourceNames.contains(source)) continue;
             final RepositoryConnection sourceConnection = getConnection(projection);
+            if (sourceConnection == null) continue; // No source exists by this name
             try {
                 // Get the cached information ...
                 CachePolicy cachePolicy = sourceConnection.getDefaultCachePolicy();
                 if (cachePolicy == null) cachePolicy = this.defaultCachePolicy;
                 DateTime expirationTime = null;
                 if (cachePolicy != null) {
-                    expirationTime = timeFactory.create(getCurrentTimeInUtc(), cachePolicy.getTimeToLive());
+                    expirationTime = getCurrentTimeInUtc().plus(cachePolicy.getTimeToLive(), TimeUnit.MILLISECONDS);
                 }
                 // Get the paths-in-source where we should fetch node contributions ...
                 Set<Path> pathsInSource = projection.getPathsInSource(path, pathFactory);
                 if (pathsInSource.isEmpty()) {
-                    // The source has no contributions ...
-                    contributions.add(Contribution.create(source, expirationTime));
+                    // The source has no contributions, but see whether the project exists BELOW this path.
+                    // We do this by getting the top-level repository paths of the projection, and then
+                    // use those to figure out the children of the nodes.
+                    Contribution contribution = null;
+                    Set<Path> topLevelPaths = projection.getTopLevelPathsInRepository(pathFactory);
+                    switch (topLevelPaths.size()) {
+                        case 0:
+                            break;
+                        case 1: {
+                            Path topLevelPath = topLevelPaths.iterator().next();
+                            if (path.isAncestorOf(topLevelPath)) {
+                                assert topLevelPath.size() > path.size();
+                                Path.Segment child = topLevelPath.getSegment(path.size());
+                                contribution = Contribution.create(source, path, expirationTime, null, child);
+                            }
+                            break;
+                        }
+                        default: {
+                            Set<Path.Segment> children = new HashSet<Path.Segment>();
+                            for (Path topLevelPath : topLevelPaths) {
+                                if (path.isAncestorOf(topLevelPath)) {
+                                    assert topLevelPath.size() > path.size();
+                                    Path.Segment child = topLevelPath.getSegment(path.size());
+                                    children.add(child);
+                                }
+                            }
+                            if (children.size() > 0) {
+                                contribution = Contribution.create(source, path, expirationTime, null, children);
+                            }
+                        }
+                    }
+                    if (contribution == null) contribution = Contribution.create(source, expirationTime);
+                    contributions.add(contribution);
                 } else {
-                    // There is at least one contribution ...
+                    // There is at least one (real) contribution ...
 
                     // Get the contributions ...
                     final int numPaths = pathsInSource.size();
@@ -357,8 +489,8 @@
                         if (!fromSource.hasError()) {
                             Collection<Property> properties = fromSource.getProperties();
                             Collection<Segment> children = fromSource.getChildren();
-                            DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : timeFactory.create(getCurrentTimeInUtc(),
-                                                                                                                         fromSource.getCachePolicy().getTimeToLive());
+                            DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : getCurrentTimeInUtc().plus(fromSource.getCachePolicy().getTimeToLive(),
+                                                                                                                                 TimeUnit.MILLISECONDS);
                             Contribution contribution = Contribution.create(source, pathInSource, expTime, properties, children);
                             contributions.add(contribution);
                         }
@@ -373,8 +505,8 @@
                             if (fromSource.hasError()) continue;
                             Collection<Property> properties = fromSource.getProperties();
                             Collection<Segment> children = fromSource.getChildren();
-                            DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : timeFactory.create(getCurrentTimeInUtc(),
-                                                                                                                         fromSource.getCachePolicy().getTimeToLive());
+                            DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : getCurrentTimeInUtc().plus(fromSource.getCachePolicy().getTimeToLive(),
+                                                                                                                                 TimeUnit.MILLISECONDS);
                             Contribution contribution = Contribution.create(source,
                                                                             fromSource.getPath(),
                                                                             expTime,
@@ -388,15 +520,140 @@
                 sourceConnection.close();
             }
         }
-        // Merge the results into a single set of results ...
-        FederatedNode mergedNode = new FederatedNode(path, UUID.randomUUID());
-        mergingStrategy.merge(mergedNode, contributions, context);
-        if (mergedNode.getCachePolicy() == null) {
-            mergedNode.setCachePolicy(defaultCachePolicy);
-        }
-        return mergedNode;
     }
 
+    // /**
+    // * Load the node at the supplied path, returning the information. This method always obtains the information from the
+    // sources
+    // * and does not use or update the cache.
+    // *
+    // * @param path the path of the node that is to be loaded
+    // * @return the federated node containing information loaded from the sources, or null if none of the sources had a
+    // * contribution
+    // * @throws InterruptedException
+    // * @throws RepositorySourceException
+    // */
+    // protected FederatedNode loadFromSources( Path path ) throws RepositorySourceException, InterruptedException {
+    // // At this point, there is no merge plan, so read information from the sources ...
+    // ExecutionContext context = getExecutionContext();
+    // PathFactory pathFactory = context.getValueFactories().getPathFactory();
+    // DateTimeFactory timeFactory = context.getValueFactories().getDateFactory();
+    // List<Contribution> contributions = new LinkedList<Contribution>();
+    // boolean foundContribution = false;
+    // for (Projection projection : this.sourceProjections) {
+    // final String source = projection.getSourceName();
+    // final RepositoryConnection sourceConnection = getConnection(projection);
+    // if (sourceConnection == null) continue; // No source exists by this name
+    // try {
+    // // Get the cached information ...
+    // CachePolicy cachePolicy = sourceConnection.getDefaultCachePolicy();
+    // if (cachePolicy == null) cachePolicy = this.defaultCachePolicy;
+    // DateTime expirationTime = null;
+    // if (cachePolicy != null) {
+    // expirationTime = getCurrentTimeInUtc().plus(cachePolicy.getTimeToLive(), TimeUnit.MILLISECONDS);
+    // }
+    // // Get the paths-in-source where we should fetch node contributions ...
+    // Set<Path> pathsInSource = projection.getPathsInSource(path, pathFactory);
+    // if (pathsInSource.isEmpty()) {
+    // // The source has no contributions, but see whether the project exists BELOW this path.
+    // // We do this by getting the top-level repository paths of the projection, and then
+    // // use those to figure out the children of the nodes.
+    // Contribution contribution = null;
+    // Set<Path> topLevelPaths = projection.getTopLevelPathsInRepository(pathFactory);
+    // switch (topLevelPaths.size()) {
+    // case 0:
+    // break;
+    // case 1: {
+    // Path topLevelPath = topLevelPaths.iterator().next();
+    // if (path.isAncestorOf(topLevelPath)) {
+    // assert topLevelPath.size() > path.size();
+    // Path.Segment child = topLevelPath.getSegment(path.size());
+    // contribution = Contribution.create(source, path, expirationTime, null, child);
+    // foundContribution = true;
+    // }
+    // break;
+    // }
+    // default: {
+    // Set<Path.Segment> children = new HashSet<Path.Segment>();
+    // for (Path topLevelPath : topLevelPaths) {
+    // if (path.isAncestorOf(topLevelPath)) {
+    // assert topLevelPath.size() > path.size();
+    // Path.Segment child = topLevelPath.getSegment(path.size());
+    // children.add(child);
+    // }
+    // }
+    // if (children.size() > 0) {
+    // contribution = Contribution.create(source, path, expirationTime, null, children);
+    // foundContribution = true;
+    // }
+    // }
+    // }
+    // if (contribution == null) contribution = Contribution.create(source, expirationTime);
+    // contributions.add(contribution);
+    // } else {
+    // // There is at least one (real) contribution ...
+    //
+    // // Get the contributions ...
+    // final int numPaths = pathsInSource.size();
+    // if (numPaths == 1) {
+    // Path pathInSource = pathsInSource.iterator().next();
+    // BasicGetNodeCommand fromSource = new BasicGetNodeCommand(pathInSource);
+    // sourceConnection.execute(getExecutionContext(), fromSource);
+    // if (!fromSource.hasError()) {
+    // Collection<Property> properties = fromSource.getProperties();
+    // Collection<Segment> children = fromSource.getChildren();
+    // DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : timeFactory.create(getCurrentTimeInUtc(),
+    // fromSource.getCachePolicy().getTimeToLive());
+    // Contribution contribution = Contribution.create(source, pathInSource, expTime, properties, children);
+    // if (!contribution.isEmpty()) foundContribution = true;
+    // contributions.add(contribution);
+    // }
+    // } else {
+    // BasicGetNodeCommand[] fromSourceCommands = new BasicGetNodeCommand[numPaths];
+    // int i = 0;
+    // for (Path pathInSource : pathsInSource) {
+    // fromSourceCommands[i++] = new BasicGetNodeCommand(pathInSource);
+    // }
+    // sourceConnection.execute(context, fromSourceCommands);
+    // for (BasicGetNodeCommand fromSource : fromSourceCommands) {
+    // if (fromSource.hasError()) continue;
+    // Collection<Property> properties = fromSource.getProperties();
+    // Collection<Segment> children = fromSource.getChildren();
+    // DateTime expTime = fromSource.getCachePolicy() == null ? expirationTime : timeFactory.create(getCurrentTimeInUtc(),
+    // fromSource.getCachePolicy().getTimeToLive());
+    // Contribution contribution = Contribution.create(source,
+    // fromSource.getPath(),
+    // expTime,
+    // properties,
+    // children);
+    // if (!contribution.isEmpty()) foundContribution = true;
+    // contributions.add(contribution);
+    // }
+    // }
+    // }
+    // } finally {
+    // sourceConnection.close();
+    // }
+    // }
+    // // If there are no contributions from any source ...
+    // if (!foundContribution) return null;
+    // if (logger.isTraceEnabled()) {
+    // logger.trace("Loaded {0} from sources, resulting in these contributions:", path);
+    // int i = 0;
+    // for (Contribution contribution : contributions) {
+    // logger.trace("  {0} {1}", ++i, contribution);
+    // }
+    // }
+    //
+    // // Merge the results into a single set of results ...
+    // FederatedNode mergedNode = new FederatedNode(path, UUID.randomUUID());
+    // mergingStrategy.merge(mergedNode, contributions, context);
+    // if (mergedNode.getCachePolicy() == null) {
+    // mergedNode.setCachePolicy(defaultCachePolicy);
+    // }
+    // return mergedNode;
+    // }
+    //
     protected MergePlan getMergePlan( BasicGetNodeCommand command ) {
         Property mergePlanProperty = command.getPropertiesByName().get(mergePlanPropertyName);
         if (mergePlanProperty == null || mergePlanProperty.isEmpty()) {

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -24,6 +24,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 
 /**
@@ -46,11 +47,11 @@
      * @param contribution4 the fourth contribution for this merge plan
      * @param contribution5 the fifth contribution for this merge plan
      */
-    public FiveContributionMergePlan( Contribution contribution1,
-                                      Contribution contribution2,
-                                      Contribution contribution3,
-                                      Contribution contribution4,
-                                      Contribution contribution5 ) {
+    /*package*/FiveContributionMergePlan( Contribution contribution1,
+                                           Contribution contribution2,
+                                           Contribution contribution3,
+                                           Contribution contribution4,
+                                           Contribution contribution5 ) {
         assert contribution1 != null;
         assert contribution2 != null;
         assert contribution3 != null;
@@ -61,6 +62,7 @@
         this.contribution3 = contribution3;
         this.contribution4 = contribution4;
         this.contribution5 = contribution5;
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
@@ -95,7 +97,7 @@
      */
     public Iterator<Contribution> iterator() {
         return new Iterator<Contribution>() {
-            private int next = 4;
+            private int next = 5;
 
             public boolean hasNext() {
                 return next > 0;
@@ -147,4 +149,14 @@
         return false;
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -24,6 +24,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 
 /**
@@ -44,10 +45,10 @@
      * @param contribution3 the third contribution for this merge plan
      * @param contribution4 the fourth contribution for this merge plan
      */
-    public FourContributionMergePlan( Contribution contribution1,
-                                      Contribution contribution2,
-                                      Contribution contribution3,
-                                      Contribution contribution4 ) {
+    /*package*/FourContributionMergePlan( Contribution contribution1,
+                                           Contribution contribution2,
+                                           Contribution contribution3,
+                                           Contribution contribution4 ) {
         assert contribution1 != null;
         assert contribution2 != null;
         assert contribution3 != null;
@@ -56,6 +57,7 @@
         this.contribution2 = contribution2;
         this.contribution3 = contribution3;
         this.contribution4 = contribution4;
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
@@ -136,4 +138,14 @@
         return false;
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(contribution1, contribution2, contribution3, contribution4);
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -24,11 +24,19 @@
 import java.io.InvalidClassException;
 import java.io.Serializable;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import net.jcip.annotations.GuardedBy;
 import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.CommonI18n;
 import org.jboss.dna.common.util.ArgCheck;
+import org.jboss.dna.common.util.StringUtil;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 import org.jboss.dna.connector.federation.contribution.EmptyContribution;
 import org.jboss.dna.spi.graph.DateTime;
@@ -54,6 +62,8 @@
     public static MergePlan create( Contribution... contributions ) {
         ArgCheck.isNotNull(contributions, "contributions");
         switch (contributions.length) {
+            case 0:
+                throw new IllegalArgumentException(CommonI18n.argumentMayNotBeEmpty.text("contributions"));
             case 1:
                 return new OneContributionMergePlan(contributions[0]);
             case 2:
@@ -74,6 +84,8 @@
         ArgCheck.isNotNull(contributions, "contributions");
         Iterator<Contribution> iter = contributions.iterator();
         switch (contributions.size()) {
+            case 0:
+                throw new IllegalArgumentException(CommonI18n.argumentMayNotBeEmpty.text("contributions"));
             case 1:
                 return new OneContributionMergePlan(iter.next());
             case 2:
@@ -91,10 +103,11 @@
 
     public static MergePlan addContribution( MergePlan plan,
                                              Contribution contribution ) {
+        ArgCheck.isNotNull(plan, "plan");
+        ArgCheck.isNotNull(contribution, "contribution");
         if (plan instanceof MultipleContributionMergePlan) {
-            MultipleContributionMergePlan multiPlan = (MultipleContributionMergePlan)plan;
-            multiPlan.addContribution(contribution);
-            return multiPlan;
+            ((MultipleContributionMergePlan)plan).addContribution(contribution);
+            return plan;
         }
         MergePlan newPlan = null;
         if (plan instanceof OneContributionMergePlan) {
@@ -160,8 +173,10 @@
      */
     private static final long serialVersionUID = 1L;
 
+    private final ReadWriteLock annotationLock = new ReentrantReadWriteLock();
+    private DateTime expirationTimeInUtc;
+    @GuardedBy( "annotationLock" )
     private Map<Name, Property> annotations = null;
-    private DateTime expirationTimeInUtc;
 
     /**
      * Create an empty merge plan
@@ -180,7 +195,7 @@
     public boolean isExpired( DateTime utcTime ) {
         assert utcTime != null;
         assert utcTime.toUtcTimeZone().equals(utcTime); // check that it is passed UTC time
-        return !expirationTimeInUtc.isAfter(utcTime);
+        return utcTime.isAfter(getExpirationTimeInUtc());
     }
 
     /**
@@ -190,6 +205,17 @@
      * @return the expiration time in UTC, or null if there is no known expiration time
      */
     public DateTime getExpirationTimeInUtc() {
+        if (expirationTimeInUtc == null) {
+            // This is computed regardless of a lock, since it's not expensive and idempotent
+            DateTime earliest = null;
+            for (Contribution contribution : this) {
+                DateTime contributionTime = contribution.getExpirationTimeInUtc();
+                if (earliest == null || (contributionTime != null && contributionTime.isBefore(earliest))) {
+                    earliest = contributionTime;
+                }
+            }
+            expirationTimeInUtc = earliest;
+        }
         return expirationTimeInUtc;
     }
 
@@ -224,8 +250,13 @@
      */
     public Property getAnnotation( Name name ) {
         if (name == null) return null;
-        if (this.annotations == null) return null;
-        return this.annotations.get(name);
+        try {
+            annotationLock.readLock().lock();
+            if (this.annotations == null) return null;
+            return this.annotations.get(name);
+        } finally {
+            annotationLock.readLock().unlock();
+        }
     }
 
     /**
@@ -239,21 +270,131 @@
      */
     public Property setAnnotation( Property annotation ) {
         if (annotation == null) return null;
-        if (this.annotations == null) {
-            this.annotations = new HashMap<Name, Property>();
+        try {
+            annotationLock.writeLock().lock();
+            if (this.annotations == null) {
+                this.annotations = new HashMap<Name, Property>();
+            }
+            return this.annotations.put(annotation.getName(), annotation);
+        } finally {
+            annotationLock.writeLock().unlock();
         }
-        return this.annotations.put(annotation.getName(), annotation);
     }
 
+    /**
+     * Get the number of annotations.
+     * 
+     * @return the number of annotations
+     */
+    public int getAnnotationCount() {
+        try {
+            annotationLock.readLock().lock();
+            if (this.annotations == null) return 0;
+            return this.annotations.size();
+        } finally {
+            annotationLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Get the set of annotation {@link Name names}.
+     * 
+     * @return the unmodifiable set of names, or an empty set if there are no annotations
+     */
+    public Set<Name> getAnnotationNames() {
+        try {
+            annotationLock.readLock().lock();
+            if (this.annotations == null) return Collections.emptySet();
+            return Collections.unmodifiableSet(this.annotations.keySet());
+        } finally {
+            annotationLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Set the annotations. This
+     * 
+     * @param annotations
+     */
     protected void setAnnotations( Map<Name, Property> annotations ) {
-        this.annotations = annotations;
+        try {
+            annotationLock.writeLock().lock();
+            this.annotations = annotations == null || annotations.isEmpty() ? null : annotations;
+        } finally {
+            annotationLock.writeLock().unlock();
+        }
     }
 
     /**
-     * @return annotations
+     * Get a copy of the annotations.
+     * 
+     * @return a copy of annotations; never null
      */
-    protected Map<Name, Property> getAnnotations() {
-        return annotations;
+    public Map<Name, Property> getAnnotations() {
+        Map<Name, Property> result = null;
+        try {
+            annotationLock.writeLock().lock();
+            if (this.annotations != null && !this.annotations.isEmpty()) {
+                result = new HashMap<Name, Property>(this.annotations);
+            } else {
+                result = Collections.emptyMap();
+            }
+        } finally {
+            annotationLock.writeLock().unlock();
+        }
+        return result;
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for (Contribution contribution : this) {
+            if (!first) {
+                first = false;
+                sb.append(", ");
+            }
+            sb.append(contribution);
+        }
+        sb.append(StringUtil.readableString(getAnnotations()));
+        return sb.toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals( Object obj ) {
+        if (obj == this) return true;
+        if (obj instanceof MergePlan) {
+            MergePlan that = (MergePlan)obj;
+            if (this.getContributionCount() != that.getContributionCount()) return false;
+            Iterator<Contribution> thisContribution = this.iterator();
+            Iterator<Contribution> thatContribution = that.iterator();
+            while (thisContribution.hasNext() && thatContribution.hasNext()) {
+                if (!thisContribution.next().equals(thatContribution.next())) return false;
+            }
+            if (this.getAnnotationCount() != that.getAnnotationCount()) return false;
+            if (!this.getAnnotations().equals(that.getAnnotations())) return false;
+            return true;
+        }
+        return false;
+    }
+
+    protected boolean checkEachContributionIsFromDistinctSource() {
+        Set<String> sourceNames = new HashSet<String>();
+        for (Contribution contribution : this) {
+            boolean added = sourceNames.add(contribution.getSourceName());
+            if (!added) return false;
+        }
+        return true;
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -34,43 +34,43 @@
 public class MultipleContributionMergePlan extends MergePlan {
 
     private static final long serialVersionUID = 1L;
-    private final List<Contribution> contributions;
+    private final List<Contribution> contributions = new CopyOnWriteArrayList<Contribution>();
 
     /**
      * @param contributions the contributions for this merge plan
      */
-    public MultipleContributionMergePlan( Contribution... contributions ) {
+    /*package*/MultipleContributionMergePlan( Contribution... contributions ) {
         assert contributions != null;
-        this.contributions = new CopyOnWriteArrayList<Contribution>();
         for (int i = 0; i != contributions.length; ++i) {
             assert contributions[i] != null;
             this.contributions.add(contributions[i]);
         }
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
      * @param contributions the contributions for this merge plan
      */
-    public MultipleContributionMergePlan( Iterable<Contribution> contributions ) {
+    /*package*/MultipleContributionMergePlan( Iterable<Contribution> contributions ) {
         assert contributions != null;
-        this.contributions = new CopyOnWriteArrayList<Contribution>();
         for (Contribution contribution : contributions) {
             assert contribution != null;
             this.contributions.add(contribution);
         }
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
      * @param contributions the contributions for this merge plan
      */
-    public MultipleContributionMergePlan( Iterator<Contribution> contributions ) {
+    /*package*/MultipleContributionMergePlan( Iterator<Contribution> contributions ) {
         assert contributions != null;
-        this.contributions = new CopyOnWriteArrayList<Contribution>();
         while (contributions.hasNext()) {
             Contribution contribution = contributions.next();
             assert contribution != null;
             this.contributions.add(contribution);
         }
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
@@ -138,4 +138,14 @@
         this.contributions.add(contribution);
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return contributions.hashCode();
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -38,7 +38,7 @@
     /**
      * @param contribution the contribution for this merge plan
      */
-    public OneContributionMergePlan( Contribution contribution ) {
+    /*package*/OneContributionMergePlan( Contribution contribution ) {
         assert contribution != null;
         this.contribution = contribution;
     }
@@ -101,4 +101,14 @@
         return contribution.getSourceName().equals(sourceName);
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return contribution.hashCode();
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/StandardMergeStrategy.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/StandardMergeStrategy.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/StandardMergeStrategy.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -50,5 +50,8 @@
 
         // Children whose identity properties are the same will be considered to be the same node ...
         assert identityPropertyName != null;
+        // Create a merge plan with the contributions ...
+        MergePlan plan = MergePlan.create(contributions);
+        federatedNode.setMergePlan(plan);
     }
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -24,6 +24,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 
 /**
@@ -42,15 +43,16 @@
      * @param contribution2 the second contribution for this merge plan
      * @param contribution3 the third contribution for this merge plan
      */
-    public ThreeContributionMergePlan( Contribution contribution1,
-                                       Contribution contribution2,
-                                       Contribution contribution3 ) {
+    /*package*/ThreeContributionMergePlan( Contribution contribution1,
+                                            Contribution contribution2,
+                                            Contribution contribution3 ) {
         assert contribution1 != null;
         assert contribution2 != null;
         assert contribution3 != null;
         this.contribution1 = contribution1;
         this.contribution2 = contribution2;
         this.contribution3 = contribution3;
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
@@ -125,4 +127,14 @@
         return false;
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(contribution1, contribution2, contribution3);
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlan.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlan.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlan.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -24,6 +24,7 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.util.HashCode;
 import org.jboss.dna.connector.federation.contribution.Contribution;
 
 /**
@@ -40,12 +41,13 @@
      * @param contribution1 the first contribution for this merge plan
      * @param contribution2 the second contribution for this merge plan
      */
-    public TwoContributionMergePlan( Contribution contribution1,
-                                     Contribution contribution2 ) {
+    /*package*/TwoContributionMergePlan( Contribution contribution1,
+                                          Contribution contribution2 ) {
         assert contribution1 != null;
         assert contribution2 != null;
         this.contribution1 = contribution1;
         this.contribution2 = contribution2;
+        assert checkEachContributionIsFromDistinctSource();
     }
 
     /**
@@ -112,4 +114,14 @@
         return contribution1.getSourceName().equals(sourceName) || contribution2.getSourceName().equals(sourceName);
     }
 
+    /**
+     * {@inheritDoc}
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return HashCode.compute(contribution1, contribution2);
+    }
+
 }

Modified: trunk/extensions/dna-connector-federation/src/main/resources/org/jboss/dna/connector/federation/FederationI18n.properties
===================================================================
--- trunk/extensions/dna-connector-federation/src/main/resources/org/jboss/dna/connector/federation/FederationI18n.properties	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/main/resources/org/jboss/dna/connector/federation/FederationI18n.properties	2008-08-18 21:38:58 UTC (rev 440)
@@ -21,6 +21,8 @@
 #
 requiredNodeDoesNotExistRelativeToNode = The required node {0} does not exist relative to {1}
 propertyIsRequired = The {0} property is required but has no value
+nodeDoesNotExistAtPath = No node exists at {0} (or below {1})
+errorRemovingNodeFromCache = Error while removing {0} from cache
 
 interruptedWhileUsingFederationConfigurationRepository = Interrupted while using federation configuration repository "{0}"
 unableToFindFederatedRepositoryInJndi = Unable to find a FederatedRepository instance in JNDI under "{1}" when creating connection to federated source "{0}"

Modified: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/FederatedRepositorySourceTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/FederatedRepositorySourceTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/FederatedRepositorySourceTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -109,7 +109,7 @@
         source.setExecutionContextFactoryJndiName(executionContextFactoryJndiName);
         source.setContext(jndiContext);
         source.setSecurityDomain(securityDomain);
-        configRepository = new SimpleRepository("Configuration Repository");
+        configRepository = SimpleRepository.get("Configuration Repository");
         configRepository.setProperty(context, "/dna:repositories/Test Repository", "dna:timeToExpire", "100000");
         configRepository.setProperty(context, "/dna:repositories/Test Repository", "dna:timeToCache", "100000");
         configRepository.setProperty(context, "/dna:repositories/Test Repository/dna:cache", "dna:projectionRules", "/ => /");

Modified: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/ProjectionTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/ProjectionTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/ProjectionTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -22,6 +22,8 @@
 package org.jboss.dna.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;
@@ -227,4 +229,50 @@
         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()));
+    }
 }

Modified: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutorTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutorTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/executor/FederatingCommandExecutorTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -21,16 +21,481 @@
  */
 package org.jboss.dna.connector.federation.executor;
 
+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.jboss.dna.common.collection.IsIteratorContaining.hasItems;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doReturn;
+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 static org.mockito.Mockito.verifyNoMoreInteractions;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.jboss.dna.connector.federation.Projection;
+import org.jboss.dna.connector.federation.ProjectionParser;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.jboss.dna.spi.ExecutionContext;
+import org.jboss.dna.spi.cache.BasicCachePolicy;
+import org.jboss.dna.spi.cache.CachePolicy;
+import org.jboss.dna.spi.connector.BasicExecutionContext;
+import org.jboss.dna.spi.connector.RepositoryConnection;
+import org.jboss.dna.spi.connector.RepositoryConnectionFactory;
+import org.jboss.dna.spi.connector.SimpleRepository;
+import org.jboss.dna.spi.connector.SimpleRepositorySource;
+import org.jboss.dna.spi.graph.DateTime;
+import org.jboss.dna.spi.graph.Path;
+import org.jboss.dna.spi.graph.PathFactory;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
 
 /**
  * @author Randall Hauch
  */
 public class FederatingCommandExecutorTest {
 
+    private FederatingCommandExecutor executor;
+    private ExecutionContext context;
+    private PathFactory pathFactory;
+    private String sourceName;
+    private Projection cacheProjection;
+    private CachePolicy cachePolicy;
+    private List<Projection> sourceProjections;
+    private Projection.Rule[] cacheProjectionRules = new Projection.Rule[] {};
+    private SimpleRepositorySource cacheSource;
+    private SimpleRepositorySource source1;
+    private SimpleRepositorySource source2;
+    private SimpleRepositorySource source3;
+    @Mock
+    private RepositoryConnectionFactory connectionFactory;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        context = new BasicExecutionContext();
+        pathFactory = context.getValueFactories().getPathFactory();
+        sourceName = "Federated Source";
+        cachePolicy = new BasicCachePolicy(10L, TimeUnit.SECONDS);
+        cacheSource = new SimpleRepositorySource();
+        cacheSource.setName("Cache");
+        cacheSource.setRepositoryName("Cache Repository");
+        ProjectionParser ruleParser = ProjectionParser.getInstance();
+        cacheProjectionRules = ruleParser.rulesFromStrings(context, "/ => /cache/repo/A");
+        cacheProjection = new Projection(cacheSource.getName(), cacheProjectionRules);
+        source1 = new SimpleRepositorySource();
+        source2 = new SimpleRepositorySource();
+        source3 = new SimpleRepositorySource();
+        source1.setName("Source 1");
+        source2.setName("Source 2");
+        source3.setName("Source 3");
+        source1.setRepositoryName("Repository 1");
+        source2.setRepositoryName("Repository 2");
+        source3.setRepositoryName("Repository 3");
+        // Set up the cache policies ...
+        source1.setDefaultCachePolicy(new BasicCachePolicy(100, TimeUnit.SECONDS));
+        source2.setDefaultCachePolicy(new BasicCachePolicy(200, TimeUnit.SECONDS));
+        source3.setDefaultCachePolicy(new BasicCachePolicy(300, TimeUnit.SECONDS));
+        sourceProjections = new ArrayList<Projection>();
+        // Source 1 projects from '/source/one/a' into repository '/a'
+        // and from '/source/one/b' into repository '/b'
+        sourceProjections.add(new Projection(source1.getName(), ruleParser.rulesFromStrings(context,
+                                                                                            "/a => /source/one/a",
+                                                                                            "/b => /source/one/b")));
+        // Source 2 projects from '/source/two/a' into repository '/a'
+        sourceProjections.add(new Projection(source2.getName(), ruleParser.rulesFromStrings(context, "/a => /source/two/a")));
+        // Source 3 projects everything into repository at root
+        sourceProjections.add(new Projection(source3.getName(), ruleParser.rulesFromStrings(context, "/ => /")));
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+        // stub(connectionFactory.createConnection(source1.getName())).toReturn(source1.getConnection());
+        doReturn(source1.getConnection()).when(connectionFactory).createConnection(source1.getName());
+        doReturn(source2.getConnection()).when(connectionFactory).createConnection(source2.getName());
+        doReturn(source3.getConnection()).when(connectionFactory).createConnection(source3.getName());
+        doReturn(cacheSource.getConnection()).when(connectionFactory).createConnection(cacheSource.getName());
+    }
+
+    @After
+    public void afterEach() throws Exception {
+        SimpleRepository.shutdownAll();
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenExecutionContextIsNull() {
+        context = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenSourceNameIsNull() {
+        sourceName = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenSourceNameIsEmpty() {
+        sourceName = "";
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenSourceNameIsBlank() {
+        sourceName = "   ";
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenConnectionFactoryIsNull() {
+        connectionFactory = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
     @Test
-    public void shouldDoSomething() {
+    public void shouldNotFailWhenCacheProjectionIsNullAndCachePolicyIsNull() {
+        cachePolicy = null;
+        cacheProjection = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
 
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCacheProjectionIsNullAndCachePolicyIsNotNull() {
+        cachePolicy = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
     }
 
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCacheProjectionIsProvidedToConstructorButCachePolicyIsNot() {
+        assertThat(cacheProjection, is(nullValue()));
+        cachePolicy = null;
+        executor = new FederatingCommandExecutor(context, sourceName, cacheProjection, cachePolicy, sourceProjections,
+                                                 connectionFactory);
+    }
+
+    @Test
+    public void shouldHaveCurrentTimeInUtc() {
+        DateTime currentTimeInUtc = executor.getCurrentTimeInUtc();
+        assertThat(currentTimeInUtc, is(notNullValue()));
+        assertThat(currentTimeInUtc.toUtcTimeZone(), is(currentTimeInUtc));
+    }
+
+    @Test
+    public void shouldReturnSameExecutionContextSuppliedToConstructor() {
+        assertThat(executor.getExecutionContext(), is(sameInstance(context)));
+    }
+
+    @Test
+    public void shouldObtainCacheConnectionFromConnectionFactoryThenHoldOntoReference() throws Exception {
+        RepositoryConnection connection = mock(RepositoryConnection.class);
+        stub(connectionFactory.createConnection(cacheSource.getName())).toReturn(connection);
+        assertThat(executor.getConnectionToCache(), is(sameInstance(connection)));
+        verify(connectionFactory, times(1)).createConnection(cacheSource.getName());
+        // Call it again, and should not ask the factory ...
+        assertThat(executor.getConnectionToCache(), is(sameInstance(connection)));
+        verifyNoMoreInteractions(connectionFactory);
+    }
+
+    @Test
+    public void shouldObtainRepositoryConnectionFromConnectionFactoryThenHoldOntoReference() throws Exception {
+        String sourceName = "Some source";
+        Projection projection = mock(Projection.class);
+        stub(projection.getSourceName()).toReturn(sourceName);
+        RepositoryConnection connection = mock(RepositoryConnection.class);
+        stub(connectionFactory.createConnection(sourceName)).toReturn(connection);
+
+        assertThat(executor.getConnection(projection), is(sameInstance(connection)));
+        verify(connectionFactory, times(1)).createConnection(sourceName);
+        // Call it again, and should not ask the factory ...
+        assertThat(executor.getConnection(projection), is(sameInstance(connection)));
+        verifyNoMoreInteractions(connectionFactory);
+    }
+
+    @Test
+    public void shouldCloseHavingNotOpenedConnections() throws Exception {
+        assertThat(executor.getOpenConnections().isEmpty(), is(true));
+        executor.close();
+        assertThat(executor.getOpenConnections().isEmpty(), is(true));
+        verifyNoMoreInteractions(connectionFactory);
+    }
+
+    @Test
+    public void shouldCloseAllOpenConnectionsWhenClosingExecutor() throws Exception {
+        // Load the connections
+        assertThat(executor.getConnectionToCache(), is(notNullValue()));
+        for (Projection projection : executor.getSourceProjections()) {
+            assertThat(executor.getConnection(projection), is(notNullValue()));
+        }
+        verify(connectionFactory).createConnection(cacheSource.getName());
+        verify(connectionFactory).createConnection(source1.getName());
+        verify(connectionFactory).createConnection(source2.getName());
+        verify(connectionFactory).createConnection(source3.getName());
+        assertThat(executor.getOpenConnections().isEmpty(), is(false));
+        // Close the executor and verify all connections have been closed
+        executor.close();
+        assertThat(executor.getOpenConnections().isEmpty(), is(true));
+        verifyNoMoreInteractions(connectionFactory);
+    }
+
+    @Test
+    public void shouldLoadContributionsForRootNodeFromSources() throws Exception {
+        Path path = pathFactory.createRootPath();
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+
+        Path.Segment aSeg = pathFactory.createSegment("a");
+        Path.Segment bSeg = pathFactory.createSegment("b");
+        assertThat(contributions.get(0).getChildren(), hasItems(aSeg, bSeg));
+        assertThat(contributions.get(1).getChildren(), hasItems(aSeg));
+        assertThat(contributions.get(2).getChildrenCount(), is(0));
+    }
+
+    @Test
+    public void shouldLoadContributionsForNonRootNodeWithOneContributionFromSources() throws Exception {
+        // Set up the content of source 3
+        SimpleRepository repository3 = SimpleRepository.get(source3.getRepositoryName());
+        repository3.setProperty(context, "/x/y", "desc", "y escription");
+        repository3.setProperty(context, "/x/y/zA", "desc", "zA description");
+        repository3.setProperty(context, "/x/y/zB", "desc", "zB description");
+        repository3.setProperty(context, "/x/y/zC", "desc", "zC description");
+
+        Path path = pathFactory.create("/x/y"); // from source 3
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+
+        Path.Segment child1 = pathFactory.createSegment("zA");
+        Path.Segment child2 = pathFactory.createSegment("zB");
+        Path.Segment child3 = pathFactory.createSegment("zC");
+        assertThat(contributions.get(0).isEmpty(), is(true));
+        assertThat(contributions.get(1).isEmpty(), is(true));
+        assertThat(contributions.get(2).getChildren(), hasItems(child1, child2, child3));
+
+        path = pathFactory.create("/x"); // from source 3
+        contributions.clear();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+
+        child1 = pathFactory.createSegment("y");
+        assertThat(contributions.get(0).isEmpty(), is(true));
+        assertThat(contributions.get(1).isEmpty(), is(true));
+        assertThat(contributions.get(2).getChildren(), hasItems(child1));
+    }
+
+    @Test
+    public void shouldLoadNonRootNodeWithTwoContributionFromSources() throws Exception {
+        // Set up the content of source 1
+        SimpleRepository repository1 = SimpleRepository.get(source1.getRepositoryName());
+        repository1.setProperty(context, "/source/one/a", "desc", "source 1 node a escription");
+        repository1.setProperty(context, "/source/one/a/nA", "desc", "source 1 node nA description");
+        repository1.setProperty(context, "/source/one/a/nB", "desc", "source 1 node nB description");
+        repository1.setProperty(context, "/source/one/a/nC", "desc", "source 1 node nC description");
+        repository1.setProperty(context, "/source/one/b", "desc", "source 1 node b description");
+        repository1.setProperty(context, "/source/one/b/pA", "desc", "source 1 node pA description");
+        repository1.setProperty(context, "/source/one/b/pB", "desc", "source 1 node pB description");
+        repository1.setProperty(context, "/source/one/b/pC", "desc", "source 1 node pC description");
+        // Set up the content of source 2
+        SimpleRepository repository2 = SimpleRepository.get(source2.getRepositoryName());
+        repository2.setProperty(context, "/source/two/a", "desc", "source 2 node a escription");
+        repository2.setProperty(context, "/source/two/a/qA", "desc", "source 2 node qA description");
+        repository2.setProperty(context, "/source/two/a/qB", "desc", "source 2 node qB description");
+        repository2.setProperty(context, "/source/two/a/qC", "desc", "source 2 node qC description");
+        // Set up the content of source 3
+        SimpleRepository repository3 = SimpleRepository.get(source3.getRepositoryName());
+        repository3.setProperty(context, "/x/y", "desc", "y escription");
+        repository3.setProperty(context, "/x/y/zA", "desc", "zA description");
+        repository3.setProperty(context, "/x/y/zB", "desc", "zB description");
+        repository3.setProperty(context, "/x/y/zC", "desc", "zC description");
+        repository3.setProperty(context, "/b/by", "desc", "by escription");
+        repository3.setProperty(context, "/b/by/bzA", "desc", "bzA description");
+        repository3.setProperty(context, "/b/by/bzB", "desc", "bzB description");
+
+        Path path = pathFactory.create("/b"); // from source 2 and source 3
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+
+        Path.Segment child1 = pathFactory.createSegment("pA");
+        Path.Segment child2 = pathFactory.createSegment("pB");
+        Path.Segment child3 = pathFactory.createSegment("pC");
+        Path.Segment child4 = pathFactory.createSegment("by");
+        assertThat(contributions.get(0).getChildren(), hasItems(child1, child2, child3));
+        assertThat(contributions.get(1).isEmpty(), is(true));
+        assertThat(contributions.get(2).getChildren(), hasItems(child4));
+
+        path = pathFactory.create("/b/by"); // from source 3
+        contributions.clear();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(2)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source3.getName()));
+
+        child1 = pathFactory.createSegment("bzA");
+        child2 = pathFactory.createSegment("bzB");
+        assertThat(contributions.get(0).isEmpty(), is(true));
+        assertThat(contributions.get(1).getChildren(), hasItems(child1));
+    }
+
+    @Test
+    public void shouldLoadNonRootNodeWithThreeContributionFromSources() throws Exception {
+        // Set up the content of source 1
+        SimpleRepository repository1 = SimpleRepository.get(source1.getRepositoryName());
+        repository1.setProperty(context, "/source/one/a", "desc", "source 1 node a escription");
+        repository1.setProperty(context, "/source/one/a/nA", "desc", "source 1 node nA description");
+        repository1.setProperty(context, "/source/one/a/nB", "desc", "source 1 node nB description");
+        repository1.setProperty(context, "/source/one/a/nC", "desc", "source 1 node nC description");
+        repository1.setProperty(context, "/source/one/b", "desc", "source 1 node b description");
+        repository1.setProperty(context, "/source/one/b/pA", "desc", "source 1 node pA description");
+        repository1.setProperty(context, "/source/one/b/pB", "desc", "source 1 node pB description");
+        repository1.setProperty(context, "/source/one/b/pC", "desc", "source 1 node pC description");
+        // Set up the content of source 2
+        SimpleRepository repository2 = SimpleRepository.get(source2.getRepositoryName());
+        repository2.setProperty(context, "/source/two/a", "desc", "source 2 node a escription");
+        repository2.setProperty(context, "/source/two/a/qA", "desc", "source 2 node qA description");
+        repository2.setProperty(context, "/source/two/a/qB", "desc", "source 2 node qB description");
+        repository2.setProperty(context, "/source/two/a/qC", "desc", "source 2 node qC description");
+        // Set up the content of source 3
+        SimpleRepository repository3 = SimpleRepository.get(source3.getRepositoryName());
+        repository3.setProperty(context, "/x/y", "desc", "y escription");
+        repository3.setProperty(context, "/x/y/zA", "desc", "zA description");
+        repository3.setProperty(context, "/x/y/zB", "desc", "zB description");
+        repository3.setProperty(context, "/x/y/zC", "desc", "zC description");
+        repository3.setProperty(context, "/b/by", "desc", "by escription");
+        repository3.setProperty(context, "/b/by/bzA", "desc", "bzA description");
+        repository3.setProperty(context, "/b/by/bzB", "desc", "bzB description");
+        repository3.setProperty(context, "/a/ay", "desc", "by escription");
+        repository3.setProperty(context, "/a/ay/azA", "desc", "bzA description");
+        repository3.setProperty(context, "/a/ay/azB", "desc", "bzB description");
+
+        Path path = pathFactory.create("/a"); // from sources 1, 2 and 3
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+
+        Path.Segment child1 = pathFactory.createSegment("nA");
+        Path.Segment child2 = pathFactory.createSegment("nB");
+        Path.Segment child3 = pathFactory.createSegment("nC");
+        Path.Segment child4 = pathFactory.createSegment("qA");
+        Path.Segment child5 = pathFactory.createSegment("qB");
+        Path.Segment child6 = pathFactory.createSegment("qC");
+        Path.Segment child7 = pathFactory.createSegment("ay");
+        assertThat(contributions.get(0).getChildren(), hasItems(child1, child2, child3));
+        assertThat(contributions.get(1).getChildren(), hasItems(child4, child5, child6));
+        assertThat(contributions.get(2).getChildren(), hasItems(child7));
+
+        path = pathFactory.create("/a/ay"); // from source 3
+        contributions.clear();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        assertThat(contributions.size(), is(1)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source3.getName()));
+        child1 = pathFactory.createSegment("azA");
+        child2 = pathFactory.createSegment("azB");
+        assertThat(contributions.get(0).getChildren(), hasItems(child1, child2));
+    }
+
+    @Test
+    public void shouldFailToLoadNodeFromSourcesWhenTheNodeDoesNotAppearInAnyOfTheSources() throws Exception {
+        Path nonExistant = pathFactory.create("/nonExistant/Node/In/AnySource");
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(nonExistant, null, contributions);
+        // All of the contributions should be empty ...
+        for (Contribution contribution : contributions) {
+            assertThat(contribution.isEmpty(), is(true));
+        }
+    }
+
+    @Test
+    public void shouldComputeCachePolicyCorrectlyUsingCurrentTimeAndSourceDefaultCachePolicy() throws Exception {
+        // Set up the content of source 1
+        SimpleRepository repository1 = SimpleRepository.get(source1.getRepositoryName());
+        repository1.setProperty(context, "/source/one/a", "desc", "source 1 node a escription");
+        repository1.setProperty(context, "/source/one/a/nA", "desc", "source 1 node nA description");
+        repository1.setProperty(context, "/source/one/a/nB", "desc", "source 1 node nB description");
+        repository1.setProperty(context, "/source/one/a/nC", "desc", "source 1 node nC description");
+        repository1.setProperty(context, "/source/one/b", "desc", "source 1 node b description");
+        repository1.setProperty(context, "/source/one/b/pA", "desc", "source 1 node pA description");
+        repository1.setProperty(context, "/source/one/b/pB", "desc", "source 1 node pB description");
+        repository1.setProperty(context, "/source/one/b/pC", "desc", "source 1 node pC description");
+        // Set up the content of source 2
+        SimpleRepository repository2 = SimpleRepository.get(source2.getRepositoryName());
+        repository2.setProperty(context, "/source/two/a", "desc", "source 2 node a escription");
+        repository2.setProperty(context, "/source/two/a/qA", "desc", "source 2 node qA description");
+        repository2.setProperty(context, "/source/two/a/qB", "desc", "source 2 node qB description");
+        repository2.setProperty(context, "/source/two/a/qC", "desc", "source 2 node qC description");
+        // Set up the content of source 3
+        SimpleRepository repository3 = SimpleRepository.get(source3.getRepositoryName());
+        repository3.setProperty(context, "/x/y", "desc", "y escription");
+        repository3.setProperty(context, "/x/y/zA", "desc", "zA description");
+        repository3.setProperty(context, "/x/y/zB", "desc", "zB description");
+        repository3.setProperty(context, "/x/y/zC", "desc", "zC description");
+        repository3.setProperty(context, "/b/by", "desc", "by escription");
+        repository3.setProperty(context, "/b/by/bzA", "desc", "bzA description");
+        repository3.setProperty(context, "/b/by/bzB", "desc", "bzB description");
+        repository3.setProperty(context, "/a/ay", "desc", "by escription");
+        repository3.setProperty(context, "/a/ay/azA", "desc", "bzA description");
+        repository3.setProperty(context, "/a/ay/azB", "desc", "bzB description");
+
+        Path path = pathFactory.create("/a"); // from sources 1, 2 and 3
+        List<Contribution> contributions = new LinkedList<Contribution>();
+        executor.loadContributionsFromSources(path, null, contributions);
+
+        // Check when the contributions expire ...
+        DateTime nowInUtc = executor.getCurrentTimeInUtc();
+        DateTime nowPlus10InUtc = nowInUtc.plusSeconds(10);
+        DateTime nowPlus110InUtc = nowInUtc.plusSeconds(110);
+        DateTime nowPlus210InUtc = nowInUtc.plusSeconds(210);
+        DateTime nowPlus310InUtc = nowInUtc.plusSeconds(310);
+        assertThat(contributions.size(), is(3)); // order is based upon order of projections
+        assertThat(contributions.get(0).getSourceName(), is(source1.getName()));
+        assertThat(contributions.get(1).getSourceName(), is(source2.getName()));
+        assertThat(contributions.get(2).getSourceName(), is(source3.getName()));
+        assertThat(contributions.get(0).isExpired(nowPlus10InUtc), is(false));
+        assertThat(contributions.get(0).isExpired(nowPlus110InUtc), is(true));
+        assertThat(contributions.get(1).isExpired(nowPlus10InUtc), is(false));
+        assertThat(contributions.get(1).isExpired(nowPlus210InUtc), is(true));
+        assertThat(contributions.get(2).isExpired(nowPlus10InUtc), is(false));
+        assertThat(contributions.get(2).isExpired(nowPlus210InUtc), is(false));
+        assertThat(contributions.get(2).isExpired(nowPlus310InUtc), is(true));
+    }
+
+    @Test
+    public void shouldGetNodeUsingPath() {
+
+    }
+
 }

Deleted: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/BasicFederatedNodeTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/BasicFederatedNodeTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/BasicFederatedNodeTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -1,41 +0,0 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2008, Red Hat Middleware LLC, and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors. 
- *
- * This is free software; you can redistribute it and/or modify it
- * 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.
- *
- * This software 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.connector.federation.merge;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * @author Randall Hauch
- */
-public class BasicFederatedNodeTest {
-
-    @Before
-    public void beforeEach() {
-    }
-
-    @Test
-    public void shouldDoSomething() {
-
-    }
-
-}

Copied: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FederatedNodeTest.java (from rev 420, trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/BasicFederatedNodeTest.java)
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FederatedNodeTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FederatedNodeTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,41 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class FederatedNodeTest {
+
+    @Before
+    public void beforeEach() {
+    }
+
+    @Test
+    public void shouldDoSomething() {
+
+    }
+
+}

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,133 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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 static org.mockito.Mockito.stub;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class FiveContributionMergePlanTest {
+
+    private FiveContributionMergePlan plan;
+    @Mock
+    private Contribution contribution1;
+    @Mock
+    private Contribution contribution2;
+    @Mock
+    private Contribution contribution3;
+    @Mock
+    private Contribution contribution4;
+    @Mock
+    private Contribution contribution5;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        stub(contribution1.getSourceName()).toReturn("source1");
+        stub(contribution2.getSourceName()).toReturn("source2");
+        stub(contribution3.getSourceName()).toReturn("source3");
+        stub(contribution4.getSourceName()).toReturn("source4");
+        stub(contribution5.getSourceName()).toReturn("source5");
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contribution1 = null;
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullSecondContribution() {
+        contribution2 = null;
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullThirdContribution() {
+        contribution3 = null;
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFourthContribution() {
+        contribution4 = null;
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFifthContribution() {
+        contribution5 = null;
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithMultipleContributionsFromTheSameSource() {
+        stub(contribution5.getSourceName()).toReturn("source1");
+        plan = new FiveContributionMergePlan(contribution1, contribution2, contribution3, contribution4, contribution5);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        assertThat(plan, hasItems(contribution1, contribution2, contribution3, contribution4, contribution5));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfFive() {
+        assertThat(plan.getContributionCount(), is(5));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        assertThat(plan.getContributionFrom(contribution1.getSourceName()), is(sameInstance(contribution1)));
+        assertThat(plan.getContributionFrom(contribution2.getSourceName()), is(sameInstance(contribution2)));
+        assertThat(plan.getContributionFrom(contribution3.getSourceName()), is(sameInstance(contribution3)));
+        assertThat(plan.getContributionFrom(contribution4.getSourceName()), is(sameInstance(contribution4)));
+        assertThat(plan.getContributionFrom(contribution5.getSourceName()), is(sameInstance(contribution5)));
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        assertThat(plan.isSource(contribution1.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution2.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution3.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution4.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution5.getSourceName()), is(true));
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FiveContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,121 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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 static org.mockito.Mockito.stub;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class FourContributionMergePlanTest {
+
+    private FourContributionMergePlan plan;
+    @Mock
+    private Contribution contribution1;
+    @Mock
+    private Contribution contribution2;
+    @Mock
+    private Contribution contribution3;
+    @Mock
+    private Contribution contribution4;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        stub(contribution1.getSourceName()).toReturn("source1");
+        stub(contribution2.getSourceName()).toReturn("source2");
+        stub(contribution3.getSourceName()).toReturn("source3");
+        stub(contribution4.getSourceName()).toReturn("source4");
+        plan = new FourContributionMergePlan(contribution1, contribution2, contribution3, contribution4);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contribution1 = null;
+        plan = new FourContributionMergePlan(contribution1, contribution2, contribution3, contribution4);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullSecondContribution() {
+        contribution2 = null;
+        plan = new FourContributionMergePlan(contribution1, contribution2, contribution3, contribution4);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullThirdContribution() {
+        contribution3 = null;
+        plan = new FourContributionMergePlan(contribution1, contribution2, contribution3, contribution4);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFourthContribution() {
+        contribution4 = null;
+        plan = new FourContributionMergePlan(contribution1, contribution2, contribution3, contribution4);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithMultipleContributionsFromTheSameSource() {
+        plan = new FourContributionMergePlan(contribution1, contribution1, contribution3, contribution4);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        assertThat(plan, hasItems(contribution1, contribution2, contribution3, contribution4));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfFour() {
+        assertThat(plan.getContributionCount(), is(4));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        assertThat(plan.getContributionFrom(contribution1.getSourceName()), is(sameInstance(contribution1)));
+        assertThat(plan.getContributionFrom(contribution2.getSourceName()), is(sameInstance(contribution2)));
+        assertThat(plan.getContributionFrom(contribution3.getSourceName()), is(sameInstance(contribution3)));
+        assertThat(plan.getContributionFrom(contribution4.getSourceName()), is(sameInstance(contribution4)));
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        assertThat(plan.isSource(contribution1.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution2.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution3.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution4.getSourceName()), is(true));
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/FourContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,330 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNot.not;
+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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.jboss.dna.spi.graph.DateTime;
+import org.jboss.dna.spi.graph.Name;
+import org.jboss.dna.spi.graph.Property;
+import org.jboss.dna.spi.graph.impl.BasicEmptyProperty;
+import org.jboss.dna.spi.graph.impl.BasicName;
+import org.jboss.dna.spi.graph.impl.JodaDateTime;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class MergePlanTest {
+
+    private MergePlan plan;
+    private List<Contribution> contributions;
+
+    @Before
+    public void beforeEach() throws Exception {
+        contributions = new ArrayList<Contribution>();
+    }
+
+    protected void addContributions( int number ) {
+        for (int i = 0; i != number; ++i) {
+            Contribution contribution = mock(Contribution.class);
+            stub(contribution.getSourceName()).toReturn("source " + i);
+            contributions.add(contribution);
+        }
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateMergePlanFromArrayWithNoContributions() {
+        MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithOneContribution() {
+        addContributions(1);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(OneContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(1));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithTwoContributions() {
+        addContributions(2);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(TwoContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(2));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithThreeContributions() {
+        addContributions(3);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(ThreeContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(3));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithFourContributions() {
+        addContributions(4);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(FourContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(4));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithFiveContributions() {
+        addContributions(5);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(FiveContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(5));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithSixContributions() {
+        addContributions(6);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(MultipleContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(6));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromArrayWithManyContributions() {
+        addContributions(100);
+        plan = MergePlan.create(contributions.toArray(new Contribution[contributions.size()]));
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(MultipleContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(100));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldFailToCreateMergePlanFromListWithNoContributions() {
+        MergePlan.create(contributions);
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithOneContribution() {
+        addContributions(1);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(OneContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(1));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithTwoContributions() {
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(TwoContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(2));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithThreeContributions() {
+        addContributions(3);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(ThreeContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(3));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithFourContributions() {
+        addContributions(4);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(FourContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(4));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithFiveContributions() {
+        addContributions(5);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(FiveContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(5));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithSixContributions() {
+        addContributions(6);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(MultipleContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(6));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldCreateMergePlanFromListWithManyContributions() {
+        addContributions(100);
+        plan = MergePlan.create(contributions);
+        assertThat(plan, is(instanceOf(MergePlan.class)));
+        assertThat(plan, is(instanceOf(MultipleContributionMergePlan.class)));
+        assertThat(plan.getContributionCount(), is(100));
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldComputeOnlyOnceTheExpirationTimeFromTheContributions() {
+        DateTime nowInUtc = new JodaDateTime().toUtcTimeZone();
+        DateTime nowPlus100InUtc = nowInUtc.plusSeconds(100);
+        DateTime nowPlus200InUtc = nowInUtc.plusSeconds(200);
+
+        addContributions(2);
+        stub(contributions.get(0).getExpirationTimeInUtc()).toReturn(nowPlus100InUtc);
+        stub(contributions.get(1).getExpirationTimeInUtc()).toReturn(nowPlus200InUtc);
+
+        plan = MergePlan.create(contributions);
+        DateTime expires = plan.getExpirationTimeInUtc();
+        assertThat(expires, is(nowPlus100InUtc));
+        verify(contributions.get(0), times(1)).getSourceName();
+        verify(contributions.get(1), times(1)).getSourceName();
+        verify(contributions.get(0), times(1)).getExpirationTimeInUtc();
+        verify(contributions.get(1), times(1)).getExpirationTimeInUtc();
+
+        DateTime expires2 = plan.getExpirationTimeInUtc();
+        assertThat(expires2, is(nowPlus100InUtc));
+        verifyNoMoreInteractions(contributions.get(0));
+        verifyNoMoreInteractions(contributions.get(1));
+    }
+
+    @Test
+    public void shouldDetermineIfExpired() {
+        DateTime nowInUtc = new JodaDateTime().toUtcTimeZone();
+        DateTime nowPlus100InUtc = nowInUtc.plusSeconds(100);
+        DateTime nowPlus200InUtc = nowInUtc.plusSeconds(200);
+        DateTime nowPlus300InUtc = nowInUtc.plusSeconds(300);
+
+        addContributions(2);
+        stub(contributions.get(0).getExpirationTimeInUtc()).toReturn(nowPlus100InUtc);
+        stub(contributions.get(1).getExpirationTimeInUtc()).toReturn(nowPlus200InUtc);
+
+        plan = MergePlan.create(contributions);
+        DateTime expires = plan.getExpirationTimeInUtc();
+        assertThat(expires, is(nowPlus100InUtc));
+        verify(contributions.get(0), times(1)).getExpirationTimeInUtc();
+        verify(contributions.get(1), times(1)).getExpirationTimeInUtc();
+        assertThat(plan.isExpired(nowInUtc), is(false));
+        assertThat(plan.isExpired(nowPlus100InUtc), is(false));
+        assertThat(plan.isExpired(nowPlus200InUtc), is(true));
+        assertThat(plan.isExpired(nowPlus300InUtc), is(true));
+    }
+
+    @Test
+    public void shouldHaveNoAnnotationsUponConstruction() {
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        assertThat(plan.getAnnotationCount(), is(0));
+    }
+
+    @Test
+    public void shouldAllowSettingAnnotationAndShouldReturnNullWhenSettingNullAnnotation() {
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        assertThat(plan.setAnnotation(null), is(nullValue()));
+    }
+
+    @Test
+    public void shouldAllowSettingAnnotationAndShouldReturnPreviousPropertyWhenSettingAnnotation() {
+        Property property = new BasicEmptyProperty(new BasicName("uri", "name"));
+        Property property2 = new BasicEmptyProperty(property.getName());
+
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        assertThat(plan.setAnnotation(property), is(nullValue()));
+        assertThat(plan.setAnnotation(property2), is(property));
+    }
+
+    @SuppressWarnings( "unchecked" )
+    @Test
+    public void shouldSetAnnotationsMapToNullIfPassedNullOrEmptyMap() {
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        plan.setAnnotations(null);
+        assertThat(plan.getAnnotationCount(), is(0));
+
+        Map<Name, Property> newAnnotations = mock(Map.class);
+        plan.setAnnotations(newAnnotations);
+        assertThat(plan.getAnnotationCount(), is(0));
+        verify(newAnnotations).size();
+    }
+
+    @SuppressWarnings( "unchecked" )
+    @Test
+    public void shouldSetAnnotationsMapToSameInstancePassedIn() {
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        Map<Name, Property> newAnnotations = mock(Map.class);
+        stub(newAnnotations.size()).toReturn(3);
+        plan.setAnnotations(newAnnotations);
+        assertThat(plan.getAnnotationCount(), is(3));
+        verify(newAnnotations).size();
+    }
+
+    @Test
+    public void shouldReturnCopyOfAnnotationsMapFromGetAnnotations() {
+        Property property = new BasicEmptyProperty(new BasicName("uri", "name"));
+        Map<Name, Property> annotations = new HashMap<Name, Property>();
+        annotations.put(property.getName(), property);
+
+        addContributions(2);
+        plan = MergePlan.create(contributions);
+        plan.setAnnotations(annotations);
+        Map<Name, Property> annotationsCopy = plan.getAnnotations();
+        assertThat(annotationsCopy, is(not(sameInstance(annotations))));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,125 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.stub;
+import java.util.LinkedList;
+import java.util.List;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class MultipleContributionMergePlanTest {
+
+    private MultipleContributionMergePlan plan;
+    private List<Contribution> contributions;
+
+    @Before
+    public void beforeEach() throws Exception {
+        contributions = new LinkedList<Contribution>();
+        addContributions(10);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    protected void addContributions( int number ) {
+        for (int i = 0; i != number; ++i) {
+            Contribution contribution = mock(Contribution.class);
+            stub(contribution.getSourceName()).toReturn("source " + i);
+            contributions.add(contribution);
+        }
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contributions.add(0, null);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullSecondContribution() {
+        contributions.add(1, null);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullThirdContribution() {
+        contributions.add(2, null);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFourthContribution() {
+        contributions.add(3, null);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFifthContribution() {
+        contributions.add(4, null);
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithMultipleContributionsFromTheSameSource() {
+        contributions.add(contributions.get(0));
+        plan = new MultipleContributionMergePlan(contributions);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        assertThat(plan, hasItems(contributions.toArray(new Contribution[contributions.size()])));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfFive() {
+        assertThat(plan.getContributionCount(), is(contributions.size()));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        for (Contribution contribution : contributions) {
+            assertThat(plan.getContributionFrom(contribution.getSourceName()), is(sameInstance(contribution)));
+        }
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        for (Contribution contribution : contributions) {
+            assertThat(plan.isSource(contribution.getSourceName()), is(true));
+        }
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/MultipleContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,88 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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.hasItem;
+import static org.mockito.Mockito.stub;
+import java.util.Iterator;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class OneContributionMergePlanTest {
+
+    private OneContributionMergePlan plan;
+    @Mock
+    private Contribution contribution1;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        plan = new OneContributionMergePlan(contribution1);
+        stub(contribution1.getSourceName()).toReturn("source");
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contribution1 = null;
+        plan = new OneContributionMergePlan(contribution1);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        Iterator<Contribution> iter = plan.iterator();
+        assertThat(iter.hasNext(), is(true));
+        assertThat(iter.next(), is(sameInstance(contribution1)));
+        assertThat(iter.hasNext(), is(false));
+        assertThat(plan, hasItem(contribution1));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfOne() {
+        assertThat(plan.getContributionCount(), is(1));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        assertThat(plan.getContributionFrom(contribution1.getSourceName()), is(sameInstance(contribution1)));
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        assertThat(plan.isSource(contribution1.getSourceName()), is(true));
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/OneContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,110 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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 static org.mockito.Mockito.stub;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class ThreeContributionMergePlanTest {
+
+    private ThreeContributionMergePlan plan;
+    @Mock
+    private Contribution contribution1;
+    @Mock
+    private Contribution contribution2;
+    @Mock
+    private Contribution contribution3;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        stub(contribution1.getSourceName()).toReturn("source1");
+        stub(contribution2.getSourceName()).toReturn("source2");
+        stub(contribution3.getSourceName()).toReturn("source3");
+        plan = new ThreeContributionMergePlan(contribution1, contribution2, contribution3);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contribution1 = null;
+        plan = new ThreeContributionMergePlan(contribution1, contribution2, contribution3);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullSecondContribution() {
+        contribution2 = null;
+        plan = new ThreeContributionMergePlan(contribution1, contribution2, contribution3);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullThirdContribution() {
+        contribution3 = null;
+        plan = new ThreeContributionMergePlan(contribution1, contribution2, contribution3);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithMultipleContributionsFromTheSameSource() {
+        plan = new ThreeContributionMergePlan(contribution1, contribution2, contribution1);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        assertThat(plan, hasItems(contribution1, contribution2, contribution3));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfThree() {
+        assertThat(plan.getContributionCount(), is(3));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        assertThat(plan.getContributionFrom(contribution1.getSourceName()), is(sameInstance(contribution1)));
+        assertThat(plan.getContributionFrom(contribution2.getSourceName()), is(sameInstance(contribution2)));
+        assertThat(plan.getContributionFrom(contribution3.getSourceName()), is(sameInstance(contribution3)));
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        assertThat(plan.isSource(contribution1.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution2.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution3.getSourceName()), is(true));
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/ThreeContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlanTest.java
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlanTest.java	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlanTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,99 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * 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.
+ *
+ * This software 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.connector.federation.merge;
+
+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 static org.mockito.Mockito.stub;
+import org.jboss.dna.connector.federation.contribution.Contribution;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * @author Randall Hauch
+ */
+public class TwoContributionMergePlanTest {
+
+    private TwoContributionMergePlan plan;
+    @Mock
+    private Contribution contribution1;
+    @Mock
+    private Contribution contribution2;
+
+    @Before
+    public void beforeEach() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        stub(contribution1.getSourceName()).toReturn("source1");
+        stub(contribution2.getSourceName()).toReturn("source2");
+        plan = new TwoContributionMergePlan(contribution1, contribution2);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullFirstContribution() {
+        contribution1 = null;
+        plan = new TwoContributionMergePlan(contribution1, contribution2);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithNullSecondContribution() {
+        contribution2 = null;
+        plan = new TwoContributionMergePlan(contribution1, contribution2);
+    }
+
+    @Test( expected = AssertionError.class )
+    public void shouldFailWhenCreatingMergePlanWithMultipleContributionsFromTheSameSource() {
+        plan = new TwoContributionMergePlan(contribution1, contribution1);
+    }
+
+    @Test
+    public void shouldReturnIteratorOverContributions() {
+        assertThat(plan, hasItems(contribution1, contribution2));
+    }
+
+    @Test
+    public void shouldHaveContributionCountOfTwo() {
+        assertThat(plan.getContributionCount(), is(2));
+    }
+
+    @Test
+    public void shouldReturnContributionWhenSuppliedNameMatchesContributionsSourceName() {
+        assertThat(plan.getContributionFrom(contribution1.getSourceName()), is(sameInstance(contribution1)));
+        assertThat(plan.getContributionFrom(contribution2.getSourceName()), is(sameInstance(contribution2)));
+    }
+
+    @Test
+    public void shouldReturnNullContributionWhenSuppliedNameDoesNotMatchContributionsSourceName() {
+        assertThat(plan.getContributionFrom("other source"), is(nullValue()));
+    }
+
+    @Test
+    public void shouldCompareSourceNameOfContributionsWhenCallingIsSource() {
+        assertThat(plan.isSource(contribution1.getSourceName()), is(true));
+        assertThat(plan.isSource(contribution2.getSourceName()), is(true));
+        assertThat(plan.isSource("other source"), is(false));
+    }
+}


Property changes on: trunk/extensions/dna-connector-federation/src/test/java/org/jboss/dna/connector/federation/merge/TwoContributionMergePlanTest.java
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Added: trunk/extensions/dna-connector-federation/src/test/resources/log4j.properties
===================================================================
--- trunk/extensions/dna-connector-federation/src/test/resources/log4j.properties	                        (rev 0)
+++ trunk/extensions/dna-connector-federation/src/test/resources/log4j.properties	2008-08-18 21:38:58 UTC (rev 440)
@@ -0,0 +1,17 @@
+# Direct log messages to stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target=System.out
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %m%n
+
+# Root logger option
+log4j.rootLogger=INFO, stdout
+
+# Set up the default logging to be INFO level, then override specific units
+log4j.logger.org.jboss.dna=INFO
+log4j.logger.org.jboss.dna.connector.federation.executor=TRACE
+
+# Jackrabbit logging
+log4j.logger.org.apache.jackrabbit=WARN, stdout
+log4j.logger.org.apache.derby=INFO, stdout
+


Property changes on: trunk/extensions/dna-connector-federation/src/test/resources/log4j.properties
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Modified: trunk/extensions/dna-connector-jbosscache/src/test/java/org/jboss/dna/connector/jbosscache/JBossCacheSourceTest.java
===================================================================
--- trunk/extensions/dna-connector-jbosscache/src/test/java/org/jboss/dna/connector/jbosscache/JBossCacheSourceTest.java	2008-08-18 18:45:57 UTC (rev 439)
+++ trunk/extensions/dna-connector-jbosscache/src/test/java/org/jboss/dna/connector/jbosscache/JBossCacheSourceTest.java	2008-08-18 21:38:58 UTC (rev 440)
@@ -32,6 +32,7 @@
 import java.util.Hashtable;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 import javax.naming.Context;
 import javax.naming.Name;
 import javax.naming.RefAddr;
@@ -153,7 +154,7 @@
     @Test
     public void shouldCreateJndiReferenceAndRecreatedObjectFromReference() throws Exception {
         BasicCachePolicy cachePolicy = new BasicCachePolicy();
-        cachePolicy.setTimeToLive(1000L);
+        cachePolicy.setTimeToLive(1000L, TimeUnit.MILLISECONDS);
         convertToAndFromJndiReference(validName,
                                       validRootNodeUuid,
                                       validCacheConfigurationName,
@@ -167,7 +168,7 @@
     @Test
     public void shouldCreateJndiReferenceAndRecreatedObjectFromReferenceWithNullProperties() throws Exception {
         BasicCachePolicy cachePolicy = new BasicCachePolicy();
-        cachePolicy.setTimeToLive(1000L);
+        cachePolicy.setTimeToLive(1000L, TimeUnit.MILLISECONDS);
         convertToAndFromJndiReference("some source", null, null, null, null, null, null, 100);
         convertToAndFromJndiReference(null, null, null, null, null, null, null, 100);
     }




More information about the dna-commits mailing list