Bug 433102 - Allow nested <dependencies> groups

Made <dependencies> element accept other <dependencies> elements as children
diff --git a/README.md b/README.md
index afc62e5..e5ffb9c 100644
--- a/README.md
+++ b/README.md
@@ -132,7 +132,7 @@
 the `<resolve>`-task, which collects the artifacts belonging to the dependencies
 transitively.
 
-    <dependency coords="g:a:v"/>
+    <dependency coords="g:a:v:scope"/>
 
     <dependency groupId="g" artifactId="a" version="v" classifier="c" type="jar" scope="runtime">
         <exclusion coords="g:a"/>
@@ -145,6 +145,11 @@
         <exclusion coords="g:a"/> <!-- global exclusion for all dependencies of this group -->
     </dependencies>
 
+    <dependencies>
+        <dependency coords="test:artifact:1.0:runtime"/>
+        <dependencies refid="deps"/> <!-- nested dependency collection merged into this one -->
+    </dependencies>
+
     <dependencies id="depsFromPom" pomRef="pom"/>
 
     <dependencies id="depsFromPlainTextFile" file="dependencies.txt"/>
diff --git a/src/main/java/org/eclipse/aether/ant/AntRepoSys.java b/src/main/java/org/eclipse/aether/ant/AntRepoSys.java
index cf81357..364173f 100644
--- a/src/main/java/org/eclipse/aether/ant/AntRepoSys.java
+++ b/src/main/java/org/eclipse/aether/ant/AntRepoSys.java
@@ -61,6 +61,7 @@
 import org.eclipse.aether.ant.types.Authentication;
 import org.eclipse.aether.ant.types.Dependencies;
 import org.eclipse.aether.ant.types.Dependency;
+import org.eclipse.aether.ant.types.DependencyContainer;
 import org.eclipse.aether.ant.types.Exclusion;
 import org.eclipse.aether.ant.types.LocalRepository;
 import org.eclipse.aether.ant.types.Mirror;
@@ -617,64 +618,7 @@
 
         if ( dependencies != null )
         {
-            List<Exclusion> globalExclusions = dependencies.getExclusions();
-            Collection<String> ids = new HashSet<String>();
-
-            for ( Dependency dep : dependencies.getDependencies() )
-            {
-                ids.add( dep.getVersionlessKey() );
-                collectRequest.addDependency( ConverterUtils.toDependency( dep, globalExclusions, session ) );
-            }
-
-            if ( dependencies.getPom() != null )
-            {
-                Model model = dependencies.getPom().getModel( task );
-                for ( org.apache.maven.model.Dependency dep : model.getDependencies() )
-                {
-                    Dependency dependency = new Dependency();
-                    dependency.setArtifactId( dep.getArtifactId() );
-                    dependency.setClassifier( dep.getClassifier() );
-                    dependency.setGroupId( dep.getGroupId() );
-                    dependency.setScope( dep.getScope() );
-                    dependency.setType( dep.getType() );
-                    dependency.setVersion( dep.getVersion() );
-                    if ( ids.contains( dependency.getVersionlessKey() ) )
-                    {
-                        project.log( "Ignoring dependency " + dependency.getVersionlessKey() + " from " + model.getId()
-                            + ", already declared locally", Project.MSG_VERBOSE );
-                        continue;
-                    }
-                    if ( dep.getSystemPath() != null && dep.getSystemPath().length() > 0 )
-                    {
-                        dependency.setSystemPath( task.getProject().resolveFile( dep.getSystemPath() ) );
-                    }
-                    for ( org.apache.maven.model.Exclusion exc : dep.getExclusions() )
-                    {
-                        Exclusion exclusion = new Exclusion();
-                        exclusion.setGroupId( exc.getGroupId() );
-                        exclusion.setArtifactId( exc.getArtifactId() );
-                        exclusion.setClassifier( "*" );
-                        exclusion.setExtension( "*" );
-                        dependency.addExclusion( exclusion );
-                    }
-                    collectRequest.addDependency( ConverterUtils.toDependency( dependency, globalExclusions, session ) );
-                }
-            }
-
-            if ( dependencies.getFile() != null )
-            {
-                List<Dependency> deps = readDependencies( dependencies.getFile() );
-                for ( Dependency dependency : deps )
-                {
-                    if ( ids.contains( dependency.getVersionlessKey() ) )
-                    {
-                        project.log( "Ignoring dependency " + dependency.getVersionlessKey() + " from "
-                                         + dependencies.getFile() + ", already declared locally", Project.MSG_VERBOSE );
-                        continue;
-                    }
-                    collectRequest.addDependency( ConverterUtils.toDependency( dependency, globalExclusions, session ) );
-                }
-            }
+            populateCollectRequest( collectRequest, task, session, dependencies, Collections.<Exclusion> emptyList() );
         }
 
         task.getProject().log( "Collecting dependencies", Project.MSG_VERBOSE );
@@ -692,6 +636,83 @@
         return result;
     }
 
+    private void populateCollectRequest( CollectRequest collectRequest, Task task, RepositorySystemSession session,
+                                         Dependencies dependencies, List<Exclusion> exclusions )
+    {
+        List<Exclusion> globalExclusions = exclusions;
+        if ( !dependencies.getExclusions().isEmpty() )
+        {
+            globalExclusions = new ArrayList<Exclusion>( exclusions );
+            globalExclusions.addAll( dependencies.getExclusions() );
+        }
+
+        Collection<String> ids = new HashSet<String>();
+
+        for ( DependencyContainer container : dependencies.getDependencyContainers() )
+        {
+            if ( container instanceof Dependency )
+            {
+                Dependency dep = (Dependency) container;
+                ids.add( dep.getVersionlessKey() );
+                collectRequest.addDependency( ConverterUtils.toDependency( dep, globalExclusions, session ) );
+            }
+            else
+            {
+                populateCollectRequest( collectRequest, task, session, (Dependencies) container, globalExclusions );
+            }
+        }
+
+        if ( dependencies.getPom() != null )
+        {
+            Model model = dependencies.getPom().getModel( task );
+            for ( org.apache.maven.model.Dependency dep : model.getDependencies() )
+            {
+                Dependency dependency = new Dependency();
+                dependency.setArtifactId( dep.getArtifactId() );
+                dependency.setClassifier( dep.getClassifier() );
+                dependency.setGroupId( dep.getGroupId() );
+                dependency.setScope( dep.getScope() );
+                dependency.setType( dep.getType() );
+                dependency.setVersion( dep.getVersion() );
+                if ( ids.contains( dependency.getVersionlessKey() ) )
+                {
+                    project.log( "Ignoring dependency " + dependency.getVersionlessKey() + " from " + model.getId()
+                        + ", already declared locally", Project.MSG_VERBOSE );
+                    continue;
+                }
+                if ( dep.getSystemPath() != null && dep.getSystemPath().length() > 0 )
+                {
+                    dependency.setSystemPath( task.getProject().resolveFile( dep.getSystemPath() ) );
+                }
+                for ( org.apache.maven.model.Exclusion exc : dep.getExclusions() )
+                {
+                    Exclusion exclusion = new Exclusion();
+                    exclusion.setGroupId( exc.getGroupId() );
+                    exclusion.setArtifactId( exc.getArtifactId() );
+                    exclusion.setClassifier( "*" );
+                    exclusion.setExtension( "*" );
+                    dependency.addExclusion( exclusion );
+                }
+                collectRequest.addDependency( ConverterUtils.toDependency( dependency, globalExclusions, session ) );
+            }
+        }
+
+        if ( dependencies.getFile() != null )
+        {
+            List<Dependency> deps = readDependencies( dependencies.getFile() );
+            for ( Dependency dependency : deps )
+            {
+                if ( ids.contains( dependency.getVersionlessKey() ) )
+                {
+                    project.log( "Ignoring dependency " + dependency.getVersionlessKey() + " from "
+                                     + dependencies.getFile() + ", already declared locally", Project.MSG_VERBOSE );
+                    continue;
+                }
+                collectRequest.addDependency( ConverterUtils.toDependency( dependency, globalExclusions, session ) );
+            }
+        }
+    }
+
     private List<Dependency> readDependencies( File file )
     {
         List<Dependency> dependencies = new ArrayList<Dependency>();
diff --git a/src/main/java/org/eclipse/aether/ant/types/Dependencies.java b/src/main/java/org/eclipse/aether/ant/types/Dependencies.java
index d26627d..35702ca 100644
--- a/src/main/java/org/eclipse/aether/ant/types/Dependencies.java
+++ b/src/main/java/org/eclipse/aether/ant/types/Dependencies.java
@@ -25,16 +25,19 @@
  */
 public class Dependencies
     extends DataType
+    implements DependencyContainer
 {
 
     private File file;
 
     private Pom pom;
 
-    private List<Dependency> dependencies = new ArrayList<Dependency>();
+    private List<DependencyContainer> containers = new ArrayList<DependencyContainer>();
 
     private List<Exclusion> exclusions = new ArrayList<Exclusion>();
 
+    private boolean nestedDependencies;
+
     protected Dependencies getRef()
     {
         return (Dependencies) getCheckedRef();
@@ -53,16 +56,20 @@
                 throw new BuildException( "A <pom> used for dependency resolution has to be backed by a pom.xml file" );
             }
             Map<String, String> ids = new HashMap<String, String>();
-            for ( Dependency dependency : dependencies )
+            for ( DependencyContainer container : containers )
             {
-                dependency.validate( task );
-                String id = dependency.getVersionlessKey();
-                String collision = ids.put( id, dependency.getVersion() );
-                if ( collision != null )
+                container.validate( task );
+                if ( container instanceof Dependency )
                 {
-                    throw new BuildException( "You must not declare multiple <dependency> elements"
-                        + " with the same coordinates but got " + id + " -> " + collision + " vs "
-                        + dependency.getVersion() );
+                    Dependency dependency = (Dependency) container;
+                    String id = dependency.getVersionlessKey();
+                    String collision = ids.put( id, dependency.getVersion() );
+                    if ( collision != null )
+                    {
+                        throw new BuildException( "You must not declare multiple <dependency> elements"
+                            + " with the same coordinates but got " + id + " -> " + collision + " vs "
+                            + dependency.getVersion() );
+                    }
                 }
             }
         }
@@ -70,7 +77,7 @@
 
     public void setRefid( Reference ref )
     {
-        if ( pom != null || !exclusions.isEmpty() || !dependencies.isEmpty() )
+        if ( pom != null || !exclusions.isEmpty() || !containers.isEmpty() )
         {
             throw noChildrenAllowed();
         }
@@ -130,21 +137,37 @@
         {
             throw new BuildException( "You must not specify both a text file and a POM to list dependencies" );
         }
+        if ( ( file != null || pom != null ) && nestedDependencies )
+        {
+            throw new BuildException( "You must not specify both a file/POM and nested dependency collections" );
+        }
     }
 
     public void addDependency( Dependency dependency )
     {
         checkChildrenAllowed();
-        this.dependencies.add( dependency );
+        containers.add( dependency );
     }
 
-    public List<Dependency> getDependencies()
+    public void addDependencies( Dependencies dependencies )
+    {
+        checkChildrenAllowed();
+        if ( dependencies == this )
+        {
+            throw circularReference();
+        }
+        containers.add( dependencies );
+        nestedDependencies = true;
+        checkExternalSources();
+    }
+
+    public List<DependencyContainer> getDependencyContainers()
     {
         if ( isReference() )
         {
-            return getRef().getDependencies();
+            return getRef().getDependencyContainers();
         }
-        return dependencies;
+        return containers;
     }
 
     public void addExclusion( Exclusion exclusion )
diff --git a/src/main/java/org/eclipse/aether/ant/types/Dependency.java b/src/main/java/org/eclipse/aether/ant/types/Dependency.java
index 4343a4a..9ca40bd 100644
--- a/src/main/java/org/eclipse/aether/ant/types/Dependency.java
+++ b/src/main/java/org/eclipse/aether/ant/types/Dependency.java
@@ -26,6 +26,7 @@
  */
 public class Dependency
     extends DataType
+    implements DependencyContainer
 {
 
     private String groupId;
diff --git a/src/main/java/org/eclipse/aether/ant/types/DependencyContainer.java b/src/main/java/org/eclipse/aether/ant/types/DependencyContainer.java
new file mode 100644
index 0000000..b05b3e5
--- /dev/null
+++ b/src/main/java/org/eclipse/aether/ant/types/DependencyContainer.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Sonatype, Inc.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Sonatype, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.aether.ant.types;
+
+import org.apache.tools.ant.Task;
+
+/**
+ */
+public interface DependencyContainer
+{
+
+    void validate( Task task );
+
+}
diff --git a/src/test/java/org/eclipse/aether/ant/ResolveTest.java b/src/test/java/org/eclipse/aether/ant/ResolveTest.java
index 921f80d..cf3d545 100644
--- a/src/test/java/org/eclipse/aether/ant/ResolveTest.java
+++ b/src/test/java/org/eclipse/aether/ant/ResolveTest.java
@@ -108,4 +108,16 @@
         assertThat( "aether-api was resolved as a property", prop, nullValue() );
     }
 
+    public void testResolveNestedDependencyCollections()
+    {
+        executeTarget( "testResolveNestedDependencyCollections" );
+
+        String prop = getProject().getProperty( "test.resolve.path.org.eclipse.aether:aether-spi:jar" );
+        assertThat( "aether-spi was not resolved as a property", prop, notNullValue() );
+        prop = getProject().getProperty( "test.resolve.path.org.eclipse.aether:aether-util:jar" );
+        assertThat( "aether-util was not resolved as a property", prop, notNullValue() );
+        prop = getProject().getProperty( "test.resolve.path.org.eclipse.aether:aether-api:jar" );
+        assertThat( "aether-api was resolved as a property", prop, nullValue() );
+    }
+
 }
diff --git a/src/test/resources/ant/Resolve/ant.xml b/src/test/resources/ant/Resolve/ant.xml
index e7c4890..4ff378f 100644
--- a/src/test/resources/ant/Resolve/ant.xml
+++ b/src/test/resources/ant/Resolve/ant.xml
@@ -63,7 +63,7 @@
   <target name="testResolveAttachments">
     <repo:resolve>
       <dependencies>
-        <dependency groupid="org.eclipse.aether" artifactid="aether-impl" version="0.9.0.M3" />
+        <dependency groupid="org.eclipse.aether" artifactid="aether-impl" version="0.9.0.v20140226" />
       </dependencies>
       <files dir="${build.dir}/resolve-attachments/" layout="javadoc/{groupId}-{artifactId}-{classifier}.{extension}" attachments="javadoc"/>
       <files dir="${build.dir}/resolve-attachments/" layout="sources/{groupId}-{artifactId}-{classifier}.{extension}" attachments="sources"/>
@@ -87,4 +87,19 @@
     </repo:resolve>
   </target>
 
+  <target name="testResolveNestedDependencyCollections">
+    <repo:resolve>
+      <dependencies>
+        <dependencies>
+          <dependency groupid="org.eclipse.aether" artifactid="aether-spi" version="0.9.0.v20140226" />
+        </dependencies>
+        <dependencies>
+          <dependency groupid="org.eclipse.aether" artifactid="aether-util" version="0.9.0.v20140226" />
+        </dependencies>
+        <exclusion coords="org.eclipse.aether:aether-api"/>
+      </dependencies>
+      <properties prefix="test.resolve.path" classpath="runtime"/>
+    </repo:resolve>
+  </target>
+
 </project>