A workspace resolver for Ant.

The code is inspired from IvyDE’s one. Work in progress.
diff --git a/src/java/org/apache/ivy/ant/AntWorkspaceResolver.java b/src/java/org/apache/ivy/ant/AntWorkspaceResolver.java
new file mode 100644
index 0000000..27cf686
--- /dev/null
+++ b/src/java/org/apache/ivy/ant/AntWorkspaceResolver.java
@@ -0,0 +1,231 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.ant;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.ivy.core.module.descriptor.Artifact;
+import org.apache.ivy.core.module.descriptor.DefaultArtifact;
+import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
+import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
+import org.apache.ivy.core.report.ArtifactDownloadReport;
+import org.apache.ivy.core.report.DownloadReport;
+import org.apache.ivy.core.report.DownloadStatus;
+import org.apache.ivy.core.resolve.DownloadOptions;
+import org.apache.ivy.core.resolve.ResolveData;
+import org.apache.ivy.core.resolve.ResolvedModuleRevision;
+import org.apache.ivy.plugins.parser.ModuleDescriptorParserRegistry;
+import org.apache.ivy.plugins.resolver.AbstractWorkspaceResolver;
+import org.apache.ivy.util.Message;
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.types.DataType;
+import org.apache.tools.ant.types.ResourceCollection;
+import org.apache.tools.ant.types.resources.FileResource;
+
+public class AntWorkspaceResolver extends DataType {
+
+    public static final class WorkspaceArtifact {
+
+        private String name;
+
+        private String type;
+
+        private String ext;
+
+        private String path;
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        public void setExt(String ext) {
+            this.ext = ext;
+        }
+
+        public void setPath(String path) {
+            this.path = path;
+        }
+    }
+
+    private List<ResourceCollection> allResources = new ArrayList<ResourceCollection>();
+
+    private boolean haltOnError = true;
+
+    private Resolver resolver;
+
+    private String name;
+
+    private List<WorkspaceArtifact> artifacts = new ArrayList<WorkspaceArtifact>();
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setHaltonerror(boolean haltOnError) {
+        this.haltOnError = haltOnError;
+    }
+
+    public void addResourceCollection(ResourceCollection resources) {
+        if (!resources.isFilesystemOnly()) {
+            throw new BuildException("Only filesystem resource collection is supported");
+        }
+        allResources.add(resources);
+    }
+
+    public WorkspaceArtifact createArtifact() {
+        WorkspaceArtifact a = new WorkspaceArtifact();
+        artifacts.add(a);
+        return a;
+    }
+
+    public Resolver getResolver() {
+        if (resolver == null) {
+            if (name == null) {
+                throw new BuildException("A name is required");
+            }
+            resolver = new Resolver();
+            resolver.setName(name);
+        }
+        return resolver;
+    }
+
+    private String getProjectName(File ivyFile) {
+        return ivyFile.getParentFile().getName();
+    }
+
+    private class Resolver extends AbstractWorkspaceResolver {
+
+        private Map<ModuleDescriptor, File> md2IvyFile;
+
+        private synchronized Map<ModuleDescriptor, File> getModuleDescriptors() {
+            if (md2IvyFile == null) {
+                md2IvyFile = new HashMap<ModuleDescriptor, File>();
+                for (ResourceCollection resources : allResources) {
+                    for (Iterator it = resources.iterator(); it.hasNext();) {
+                        File ivyFile = ((FileResource) it.next()).getFile();
+                        try {
+                            ModuleDescriptor md = ModuleDescriptorParserRegistry.getInstance()
+                                    .parseDescriptor(getParserSettings(), ivyFile.toURI().toURL(),
+                                        isValidate());
+                            md2IvyFile.put(md, ivyFile);
+                            Message.debug("Add " + md.getModuleRevisionId().getModuleId());
+                        } catch (Exception ex) {
+                            if (haltOnError) {
+                                throw new BuildException("impossible to parse ivy file " + ivyFile
+                                        + " exception=" + ex, ex);
+                            } else {
+                                Message.warn("impossible to parse ivy file " + ivyFile
+                                        + " exception=" + ex.getMessage());
+                            }
+                        }
+                    }
+                }
+            }
+            return md2IvyFile;
+        }
+
+        public ResolvedModuleRevision getDependency(DependencyDescriptor dd, ResolveData data)
+                throws ParseException {
+            Map<ModuleDescriptor, File> mds = getModuleDescriptors();
+            for (Entry<ModuleDescriptor, File> md : mds.entrySet()) {
+                ResolvedModuleRevision rmr = checkCandidate(dd, md.getKey(),
+                    getProjectName(md.getValue()));
+                if (rmr != null) {
+                    return rmr;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        protected List<Artifact> createWorkspaceArtifacts(ModuleDescriptor md) {
+            List<Artifact> res = new ArrayList<Artifact>();
+
+            for (WorkspaceArtifact wa : artifacts) {
+                String name = wa.name;
+                String type = wa.type;
+                String ext = wa.ext;
+                String path = wa.path;
+                if (name == null) {
+                    name = md.getModuleRevisionId().getName();
+                }
+                if (type == null) {
+                    type = "jar";
+                }
+                if (ext == null) {
+                    ext = "jar";
+                }
+                if (path == null) {
+                    path = "target" + File.separator + "dist" + File.separator + type + "s"
+                            + File.separator + name + "." + ext;
+                }
+
+                URL url;
+                File ivyFile = md2IvyFile.get(md);
+                File artifactFile = new File(ivyFile.getParentFile(), path);
+                try {
+                    url = artifactFile.toURI().toURL();
+                } catch (MalformedURLException e) {
+                    throw new RuntimeException("Unsupported file path : " + artifactFile, e);
+                }
+
+                res.add(new DefaultArtifact(md.getModuleRevisionId(), new Date(), name, type, ext,
+                        url, null));
+            }
+
+            return res;
+        }
+
+        public DownloadReport download(Artifact[] artifacts, DownloadOptions options) {
+            // Not much to do here - downloads are not required for workspace projects.
+            DownloadReport dr = new DownloadReport();
+            for (int i = 0; i < artifacts.length; i++) {
+                ArtifactDownloadReport adr = new ArtifactDownloadReport(artifacts[i]);
+                dr.addArtifactReport(adr);
+                adr.setDownloadStatus(DownloadStatus.NO);
+                adr.setSize(0);
+                URL url = artifacts[i].getUrl();
+                File f;
+                try {
+                    f = new File(url.toURI());
+                } catch (URISyntaxException e) {
+                    f = new File(url.getPath());
+                }
+                adr.setLocalFile(f);
+                Message.verbose("\t[IN WORKSPACE] " + artifacts[i]);
+            }
+            return dr;
+        }
+    }
+
+}
diff --git a/src/java/org/apache/ivy/ant/IvyAntSettings.java b/src/java/org/apache/ivy/ant/IvyAntSettings.java
index 660ebbe..bb67ba7 100644
--- a/src/java/org/apache/ivy/ant/IvyAntSettings.java
+++ b/src/java/org/apache/ivy/ant/IvyAntSettings.java
@@ -102,6 +102,8 @@
 
     private boolean autoRegistered = false;
 
+    private AntWorkspaceResolver antWorkspaceResolver;
+
     /**
      * Returns the default ivy settings of this classloader. If it doesn't exist yet, a new one is
      * created using the given project to back the VariableContainer.
@@ -275,6 +277,10 @@
             defineDefaultSettingFile(ivyAntVariableContainer, task);
         }
 
+        if (antWorkspaceResolver != null) {
+            settings.addConfigured(antWorkspaceResolver.getResolver());
+        }
+
         Ivy ivy = Ivy.newInstance(settings);
         try {
             ivy.pushContext();
@@ -388,4 +394,7 @@
         URLHandlerRegistry.setDefault(dispatcher);
     }
 
+    public void addConfiguredWorkspaceResolver(AntWorkspaceResolver antWorkspaceResolver) {
+        this.antWorkspaceResolver = antWorkspaceResolver;
+    }
 }
diff --git a/src/java/org/apache/ivy/ant/IvyConfigure.java b/src/java/org/apache/ivy/ant/IvyConfigure.java
index 5e05adc..6926d02 100644
--- a/src/java/org/apache/ivy/ant/IvyConfigure.java
+++ b/src/java/org/apache/ivy/ant/IvyConfigure.java
@@ -130,6 +130,11 @@
         settings.setPasswd(passwd);
     }
 
+    public void addConfiguredWorkspaceResolver(AntWorkspaceResolver resolver) {
+        settings.addConfiguredWorkspaceResolver(resolver);
+    }
+
+    @Override
     public void execute() throws BuildException {
         String settingsId = settings.getId();
         Object otherRef = getProject().getReference(settingsId);
diff --git a/src/java/org/apache/ivy/ant/antlib.xml b/src/java/org/apache/ivy/ant/antlib.xml
index ac32740..0111f78 100644
--- a/src/java/org/apache/ivy/ant/antlib.xml
+++ b/src/java/org/apache/ivy/ant/antlib.xml
@@ -49,4 +49,5 @@
     <taskdef name="fixdeps" classname="org.apache.ivy.ant.FixDepsTask" />
     <taskdef name="dependencytree" classname="org.apache.ivy.ant.IvyDependencyTree"/>
     <taskdef name="checkdepsupdate" classname="org.apache.ivy.ant.IvyDependencyUpdateChecker"/>
+    <typedef name="workspaceresolver" classname="org.apache.ivy.ant.AntWorkspaceResolver" />
 </antlib>
diff --git a/src/java/org/apache/ivy/core/module/descriptor/DefaultWorkspaceModuleDescriptor.java b/src/java/org/apache/ivy/core/module/descriptor/DefaultWorkspaceModuleDescriptor.java
new file mode 100644
index 0000000..1c86f56
--- /dev/null
+++ b/src/java/org/apache/ivy/core/module/descriptor/DefaultWorkspaceModuleDescriptor.java
@@ -0,0 +1,42 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.core.module.descriptor;
+
+import java.util.Date;
+
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
+import org.apache.ivy.plugins.repository.Resource;
+
+public class DefaultWorkspaceModuleDescriptor extends DefaultModuleDescriptor implements
+        WorkspaceModuleDescriptor {
+
+    public DefaultWorkspaceModuleDescriptor(ModuleDescriptorParser parser, Resource res) {
+        super(parser, res);
+    }
+
+    public DefaultWorkspaceModuleDescriptor(ModuleRevisionId id, String status, Date pubDate) {
+        super(id, status, pubDate);
+    }
+
+    public DefaultWorkspaceModuleDescriptor(ModuleRevisionId id, String status, Date pubDate,
+            boolean isDefault) {
+        super(id, status, pubDate, isDefault);
+    }
+
+}
diff --git a/src/java/org/apache/ivy/core/module/descriptor/WorkspaceModuleDescriptor.java b/src/java/org/apache/ivy/core/module/descriptor/WorkspaceModuleDescriptor.java
new file mode 100644
index 0000000..4403526
--- /dev/null
+++ b/src/java/org/apache/ivy/core/module/descriptor/WorkspaceModuleDescriptor.java
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.core.module.descriptor;
+
+/**
+ * Marker interface for module descriptor of a module in a workspace, then having special semantics,
+ * like artifacts declared by the resolver rather than the descriptor
+ */
+public interface WorkspaceModuleDescriptor extends ModuleDescriptor {
+
+}
diff --git a/src/java/org/apache/ivy/core/resolve/IvyNodeUsage.java b/src/java/org/apache/ivy/core/resolve/IvyNodeUsage.java
index b6631f2..cb0eafe 100644
--- a/src/java/org/apache/ivy/core/resolve/IvyNodeUsage.java
+++ b/src/java/org/apache/ivy/core/resolve/IvyNodeUsage.java
@@ -27,6 +27,7 @@
 import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor;
 import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
 import org.apache.ivy.core.module.descriptor.IncludeRule;
+import org.apache.ivy.core.module.descriptor.WorkspaceModuleDescriptor;
 
 /**
  * Class collecting usage data for an IvyNode.
@@ -219,6 +220,10 @@
     }
 
     protected Set<DependencyArtifactDescriptor> getDependencyArtifactsSet(String rootModuleConf) {
+        if (node.getDescriptor() instanceof WorkspaceModuleDescriptor) {
+            // for a module in the "workspace" artifacts will be actually declared by the resolver
+            return null;
+        }
         Collection<Depender> dependersInConf = dependers.get(rootModuleConf);
         if (dependersInConf == null) {
             return null;
diff --git a/src/java/org/apache/ivy/core/settings/IvySettings.java b/src/java/org/apache/ivy/core/settings/IvySettings.java
index de806bf..5113cf5 100644
--- a/src/java/org/apache/ivy/core/settings/IvySettings.java
+++ b/src/java/org/apache/ivy/core/settings/IvySettings.java
@@ -76,6 +76,7 @@
 import org.apache.ivy.plugins.latest.LatestRevisionStrategy;
 import org.apache.ivy.plugins.latest.LatestStrategy;
 import org.apache.ivy.plugins.latest.LatestTimeStrategy;
+import org.apache.ivy.plugins.latest.WorkspaceLatestStrategy;
 import org.apache.ivy.plugins.lock.CreateFileLockStrategy;
 import org.apache.ivy.plugins.lock.LockStrategy;
 import org.apache.ivy.plugins.lock.NIOFileLockStrategy;
@@ -92,10 +93,12 @@
 import org.apache.ivy.plugins.report.LogReportOutputter;
 import org.apache.ivy.plugins.report.ReportOutputter;
 import org.apache.ivy.plugins.report.XmlReportOutputter;
+import org.apache.ivy.plugins.resolver.AbstractWorkspaceResolver;
 import org.apache.ivy.plugins.resolver.ChainResolver;
 import org.apache.ivy.plugins.resolver.DependencyResolver;
 import org.apache.ivy.plugins.resolver.DualResolver;
 import org.apache.ivy.plugins.resolver.ResolverSettings;
+import org.apache.ivy.plugins.resolver.WorkspaceChainResolver;
 import org.apache.ivy.plugins.signer.SignatureGenerator;
 import org.apache.ivy.plugins.trigger.Trigger;
 import org.apache.ivy.plugins.version.ChainVersionMatcher;
@@ -207,6 +210,8 @@
 
     private PackingRegistry packingRegistry = new PackingRegistry();
 
+    private AbstractWorkspaceResolver workspaceResolver;
+
     public IvySettings() {
         this(new IvyVariableContainerImpl());
     }
@@ -879,9 +884,20 @@
         dictatorResolver = resolver;
     }
 
+    private DependencyResolver getDictatorResolver() {
+        if (dictatorResolver == null) {
+            return null;
+        }
+        if (workspaceResolver != null && !(dictatorResolver instanceof WorkspaceChainResolver)) {
+            dictatorResolver = new WorkspaceChainResolver(this, dictatorResolver, workspaceResolver);
+        }
+        return dictatorResolver;
+    }
+
     public synchronized DependencyResolver getResolver(ModuleRevisionId mrid) {
-        if (dictatorResolver != null) {
-            return dictatorResolver;
+        DependencyResolver r = getDictatorResolver();
+        if (r != null) {
+            return r;
         }
         String resolverName = getResolverName(mrid);
         return getResolver(resolverName);
@@ -892,23 +908,32 @@
     }
 
     public synchronized DependencyResolver getResolver(String resolverName) {
-        if (dictatorResolver != null) {
-            return dictatorResolver;
+        DependencyResolver r = getDictatorResolver();
+        if (r != null) {
+            return r;
         }
         DependencyResolver resolver = resolversMap.get(resolverName);
         if (resolver == null) {
             Message.error("unknown resolver " + resolverName);
         }
+        if (workspaceResolver != null && !(resolver instanceof WorkspaceChainResolver)) {
+            resolver = new WorkspaceChainResolver(this, resolver, workspaceResolver);
+            resolversMap.put(resolverName, resolver);
+        }
         return resolver;
     }
 
     public synchronized DependencyResolver getDefaultResolver() {
-        if (dictatorResolver != null) {
-            return dictatorResolver;
+        DependencyResolver r = getDictatorResolver();
+        if (r != null) {
+            return r;
         }
         if (defaultResolver == null) {
             defaultResolver = resolversMap.get(defaultResolverName);
         }
+        if (workspaceResolver != null && !(defaultResolver instanceof WorkspaceChainResolver)) {
+            defaultResolver = new WorkspaceChainResolver(this, defaultResolver, workspaceResolver);
+        }
         return defaultResolver;
     }
 
@@ -997,7 +1022,12 @@
         if ("default".equals(name)) {
             return getDefaultLatestStrategy();
         }
-        return latestStrategies.get(name);
+        LatestStrategy strategy = latestStrategies.get(name);
+        if (workspaceResolver != null && !(strategy instanceof WorkspaceLatestStrategy)) {
+            strategy = new WorkspaceLatestStrategy(strategy);
+            latestStrategies.put(name, strategy);
+        }
+        return strategy;
     }
 
     public synchronized void addLatestStrategy(String name, LatestStrategy latest) {
@@ -1224,6 +1254,10 @@
         if (defaultLatestStrategy == null) {
             defaultLatestStrategy = new LatestRevisionStrategy();
         }
+        if (workspaceResolver != null
+                && !(defaultLatestStrategy instanceof WorkspaceLatestStrategy)) {
+            defaultLatestStrategy = new WorkspaceLatestStrategy(defaultLatestStrategy);
+        }
         return defaultLatestStrategy;
     }
 
@@ -1500,4 +1534,20 @@
     public PackingRegistry getPackingRegistry() {
         return packingRegistry;
     }
+
+    public void addConfigured(AbstractWorkspaceResolver workspaceResolver) {
+        this.workspaceResolver = workspaceResolver;
+        if (workspaceResolver != null) {
+            workspaceResolver.setSettings(this);
+            DefaultRepositoryCacheManager cacheManager = new DefaultRepositoryCacheManager();
+            String cacheName = "workspace-resolver-cache-" + workspaceResolver.getName();
+            cacheManager.setBasedir(new File(getDefaultCache(), cacheName));
+            cacheManager.setCheckmodified(true);
+            cacheManager.setUseOrigin(true);
+            cacheManager.setName(cacheName);
+            addRepositoryCacheManager(cacheManager);
+            workspaceResolver.setCache(cacheName);
+        }
+
+    }
 }
diff --git a/src/java/org/apache/ivy/plugins/latest/WorkspaceLatestStrategy.java b/src/java/org/apache/ivy/plugins/latest/WorkspaceLatestStrategy.java
new file mode 100644
index 0000000..64ef7ec
--- /dev/null
+++ b/src/java/org/apache/ivy/plugins/latest/WorkspaceLatestStrategy.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.plugins.latest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A strategy which delegate to another strategy, unless for the latest and working revisions which
+ * are considered as superior to any other revision. < br/>
+ * NB : it is for internal usage of Ivy only!
+ */
+public class WorkspaceLatestStrategy extends AbstractLatestStrategy {
+
+    private LatestStrategy delegate;
+
+    public WorkspaceLatestStrategy(LatestStrategy delegate) {
+        this.delegate = delegate;
+        setName("workspace-" + delegate.getName());
+    }
+
+    public List<ArtifactInfo> sort(ArtifactInfo[] infos) {
+        List<ArtifactInfo> sorted = delegate.sort(infos);
+
+        List<ArtifactInfo> head = new ArrayList<ArtifactInfo>();
+        List<ArtifactInfo> tail = new ArrayList<ArtifactInfo>();
+
+        for (ArtifactInfo ai : sorted) {
+            String rev = ai.getRevision();
+            boolean latestRev = rev.startsWith("latest") || rev.startsWith("working");
+            if (latestRev) {
+                head.add(ai);
+            } else {
+                tail.add(ai);
+            }
+        }
+
+        head.addAll(tail);
+        return head;
+    }
+
+}
\ No newline at end of file
diff --git a/src/java/org/apache/ivy/plugins/resolver/AbstractWorkspaceResolver.java b/src/java/org/apache/ivy/plugins/resolver/AbstractWorkspaceResolver.java
new file mode 100644
index 0000000..5444ab0
--- /dev/null
+++ b/src/java/org/apache/ivy/plugins/resolver/AbstractWorkspaceResolver.java
@@ -0,0 +1,239 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.plugins.resolver;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.List;
+
+import org.apache.ivy.Ivy;
+import org.apache.ivy.core.module.descriptor.Artifact;
+import org.apache.ivy.core.module.descriptor.Configuration;
+import org.apache.ivy.core.module.descriptor.DefaultArtifact;
+import org.apache.ivy.core.module.descriptor.DefaultWorkspaceModuleDescriptor;
+import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
+import org.apache.ivy.core.module.descriptor.ExcludeRule;
+import org.apache.ivy.core.module.descriptor.License;
+import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
+import org.apache.ivy.core.module.descriptor.WorkspaceModuleDescriptor;
+import org.apache.ivy.core.module.id.ModuleId;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.DownloadStatus;
+import org.apache.ivy.core.report.MetadataArtifactDownloadReport;
+import org.apache.ivy.core.resolve.ResolveData;
+import org.apache.ivy.core.resolve.ResolvedModuleRevision;
+import org.apache.ivy.osgi.core.BundleInfo;
+import org.apache.ivy.osgi.core.ManifestHeaderElement;
+import org.apache.ivy.osgi.core.ManifestHeaderValue;
+import org.apache.ivy.plugins.resolver.util.ResolvedResource;
+import org.apache.ivy.plugins.version.VersionMatcher;
+import org.apache.ivy.util.Message;
+
+public abstract class AbstractWorkspaceResolver extends AbstractResolver {
+
+    private boolean ignoreBranch;
+
+    private boolean ignoreVersion;
+
+    public void setIgnoreBranch(boolean ignoreBranch) {
+        this.ignoreBranch = ignoreBranch;
+    }
+
+    public void setIgnoreVersion(boolean ignoreVersion) {
+        this.ignoreVersion = ignoreVersion;
+    }
+
+    protected ResolvedModuleRevision checkCandidate(DependencyDescriptor dd, ModuleDescriptor md,
+            String workspaceModuleName) {
+
+        if (workspaceModuleName == null) {
+            workspaceModuleName = dd.getDependencyId().toString();
+        }
+
+        ModuleRevisionId dependencyMrid = dd.getDependencyRevisionId();
+        String org = dependencyMrid.getModuleId().getOrganisation();
+        String module = dependencyMrid.getModuleId().getName();
+
+        VersionMatcher versionMatcher = getSettings().getVersionMatcher();
+
+        ModuleRevisionId candidateMrid = md.getModuleRevisionId();
+
+        // search a match on the organization and the module name
+
+        if (org.equals(BundleInfo.BUNDLE_TYPE)) {
+            // looking for an OSGi bundle via its symbolic name
+            String sn = md.getExtraInfoContentByTagName("Bundle-SymbolicName");
+            if (sn == null || !module.equals(sn)) {
+                // not found, skip to next
+                return null;
+            }
+        } else if (org.equals(BundleInfo.PACKAGE_TYPE)) {
+            // looking for an OSGi bundle via its exported package
+            String exportedPackages = md.getExtraInfoContentByTagName("Export-Package");
+            if (exportedPackages == null) {
+                // not found, skip to next
+                return null;
+            }
+            boolean found = false;
+            String version = null;
+            ManifestHeaderValue exportElements;
+            try {
+                exportElements = new ManifestHeaderValue(exportedPackages);
+            } catch (ParseException e) {
+                // wrong OSGi header: skip it
+                return null;
+            }
+            for (ManifestHeaderElement exportElement : exportElements.getElements()) {
+                if (exportElement.getValues().contains(module)) {
+                    found = true;
+                    version = exportElement.getAttributes().get("version");
+                    break;
+                }
+            }
+            if (!found) {
+                // not found, skip to next
+                return null;
+            }
+            if (version == null) {
+                // no version means anything can match. Let's trick the version matcher by
+                // setting the exact expected version
+                version = dependencyMrid.getRevision();
+            }
+            md.setResolvedModuleRevisionId(ModuleRevisionId.newInstance(org, module, version));
+        } else {
+            if (!candidateMrid.getModuleId().equals(dependencyMrid.getModuleId())) {
+                // it doesn't match org#module, skip to next
+                return null;
+            }
+        }
+
+        Message.verbose("Workspace resolver found potential matching workspace module "
+                + workspaceModuleName + " with module " + candidateMrid + " for module "
+                + dependencyMrid);
+
+        if (!ignoreBranch) {
+            ModuleId mid = dependencyMrid.getModuleId();
+            String defaultBranch = getSettings().getDefaultBranch(mid);
+            String dependencyBranch = dependencyMrid.getBranch();
+            String candidateBranch = candidateMrid.getBranch();
+            if (dependencyBranch == null) {
+                dependencyBranch = defaultBranch;
+            }
+            if (candidateBranch == null) {
+                candidateBranch = defaultBranch;
+            }
+            if (dependencyBranch != candidateBranch) {
+                // Both cannot be null
+                if (dependencyBranch == null || candidateBranch == null) {
+                    Message.verbose("\t\trejected since branches doesn't match (one is set, the other isn't)");
+                    return null;
+                }
+                if (!dependencyBranch.equals(candidateBranch)) {
+                    Message.verbose("\t\trejected since branches doesn't match");
+                    return null;
+                }
+            }
+        }
+
+        // Found one; check if it is for the module we need
+        if (!ignoreVersion
+                && !md.getModuleRevisionId().getRevision().equals(Ivy.getWorkingRevision())
+                && !versionMatcher.accept(dd.getDependencyRevisionId(), md)) {
+            Message.verbose("\t\treject as version didn't match");
+            return null;
+        }
+
+        if (ignoreVersion) {
+            Message.verbose("\t\tmatched (version are ignored)");
+        } else {
+            Message.verbose("\t\tversion matched");
+        }
+
+        WorkspaceModuleDescriptor workspaceMd = createWorkspaceMd(md);
+
+        Artifact mdaf = md.getMetadataArtifact();
+        if (mdaf == null) {
+            mdaf = new DefaultArtifact(md.getModuleRevisionId(), md.getPublicationDate(),
+                    workspaceModuleName, "ivy", "");
+        }
+        MetadataArtifactDownloadReport madr = new MetadataArtifactDownloadReport(mdaf);
+        madr.setDownloadStatus(DownloadStatus.SUCCESSFUL);
+        madr.setSearched(true);
+
+        return new ResolvedModuleRevision(this, this, workspaceMd, madr);
+    }
+
+    protected WorkspaceModuleDescriptor createWorkspaceMd(ModuleDescriptor md) {
+        DefaultWorkspaceModuleDescriptor newMd = new DefaultWorkspaceModuleDescriptor(
+                md.getModuleRevisionId(), "release", null, true);
+        newMd.addConfiguration(new Configuration(ModuleDescriptor.DEFAULT_CONFIGURATION));
+        newMd.setLastModified(System.currentTimeMillis());
+
+        newMd.setDescription(md.getDescription());
+        newMd.setHomePage(md.getHomePage());
+        newMd.setLastModified(md.getLastModified());
+        newMd.setPublicationDate(md.getPublicationDate());
+        newMd.setResolvedPublicationDate(md.getResolvedPublicationDate());
+        newMd.setStatus(md.getStatus());
+
+        List<Artifact> artifacts = createWorkspaceArtifacts(md);
+
+        Configuration[] allConfs = md.getConfigurations();
+        for (Artifact af : artifacts) {
+            if (allConfs.length == 0) {
+                newMd.addArtifact(ModuleDescriptor.DEFAULT_CONFIGURATION, af);
+            } else {
+                for (int k = 0; k < allConfs.length; k++) {
+                    newMd.addConfiguration(allConfs[k]);
+                    newMd.addArtifact(allConfs[k].getName(), af);
+                }
+            }
+        }
+
+        DependencyDescriptor[] dependencies = md.getDependencies();
+        for (int k = 0; k < dependencies.length; k++) {
+            newMd.addDependency(dependencies[k]);
+        }
+
+        ExcludeRule[] allExcludeRules = md.getAllExcludeRules();
+        for (int k = 0; k < allExcludeRules.length; k++) {
+            newMd.addExcludeRule(allExcludeRules[k]);
+        }
+
+        newMd.getExtraInfos().addAll(md.getExtraInfos());
+
+        License[] licenses = md.getLicenses();
+        for (int k = 0; k < licenses.length; k++) {
+            newMd.addLicense(licenses[k]);
+        }
+
+        return newMd;
+    }
+
+    abstract protected List<Artifact> createWorkspaceArtifacts(ModuleDescriptor md);
+
+    public void publish(Artifact artifact, File src, boolean overwrite) throws IOException {
+        throw new UnsupportedOperationException("publish not supported by " + getName());
+    }
+
+    public ResolvedResource findIvyFileRef(DependencyDescriptor dd, ResolveData data) {
+        return null;
+    }
+
+}
diff --git a/src/java/org/apache/ivy/plugins/resolver/WorkspaceChainResolver.java b/src/java/org/apache/ivy/plugins/resolver/WorkspaceChainResolver.java
new file mode 100644
index 0000000..a3662cf
--- /dev/null
+++ b/src/java/org/apache/ivy/plugins/resolver/WorkspaceChainResolver.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.plugins.resolver;
+
+import org.apache.ivy.core.settings.IvySettings;
+
+/**
+ * Resolver which decorate normal resolver so that the workspace resolver can hijack the resolve
+ * process <br />
+ * NB : it is for internal usage of Ivy only!
+ */
+public class WorkspaceChainResolver extends ChainResolver {
+
+    public WorkspaceChainResolver(IvySettings settings, DependencyResolver delegate,
+            AbstractWorkspaceResolver workspaceResolver) {
+        setName("workspace-chain-" + delegate.getName());
+        setSettings(settings);
+        setReturnFirst(true);
+        add(workspaceResolver);
+        add(delegate);
+    }
+}
diff --git a/test/java/org/apache/ivy/ant/AntBuildResolverTest.java b/test/java/org/apache/ivy/ant/AntBuildResolverTest.java
new file mode 100644
index 0000000..6eb3c79
--- /dev/null
+++ b/test/java/org/apache/ivy/ant/AntBuildResolverTest.java
@@ -0,0 +1,213 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ivy.ant;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+
+import org.apache.ivy.ant.AntWorkspaceResolver.WorkspaceArtifact;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.DownloadStatus;
+import org.apache.ivy.core.report.ResolveReport;
+import org.apache.tools.ant.DefaultLogger;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.Delete;
+import org.apache.tools.ant.types.FileSet;
+import org.apache.tools.ant.types.Path;
+
+public class AntBuildResolverTest extends TestCase {
+
+    private static final ModuleRevisionId MRID_MODULE1 = ModuleRevisionId.newInstance("org.acme",
+        "module1", "1.1");
+
+    private static final ModuleRevisionId MRID_PROJECT1 = ModuleRevisionId.newInstance(
+        "org.apache.ivy.test", "project1", "0.1");
+
+    private File cache;
+
+    private Project project;
+
+    private IvyConfigure configure;
+
+    private WorkspaceArtifact wa;
+
+    @Override
+    protected void setUp() throws Exception {
+        createCache();
+        project = new Project();
+        DefaultLogger logger = new DefaultLogger();
+        logger.setMessageOutputLevel(Project.MSG_INFO);
+        logger.setOutputPrintStream(System.out);
+        logger.setErrorPrintStream(System.err);
+        project.addBuildListener(logger);
+        project.setProperty("ivy.cache.dir", cache.getAbsolutePath());
+
+        AntWorkspaceResolver antWorkspaceResolver = new AntWorkspaceResolver();
+        antWorkspaceResolver.setName("test-workspace");
+        wa = antWorkspaceResolver.createArtifact();
+        FileSet fileset = new FileSet();
+        fileset.setProject(project);
+        fileset.setDir(new File("test/workspace"));
+        fileset.setIncludes("*/ivy.xml");
+        antWorkspaceResolver.addResourceCollection(fileset);
+        antWorkspaceResolver.setProject(project);
+
+        configure = new IvyConfigure();
+        configure.setProject(project);
+        configure.setFile(new File("test/workspace/ivysettings.xml"));
+        configure.addConfiguredWorkspaceResolver(antWorkspaceResolver);
+        configure.execute();
+    }
+
+    private void createCache() {
+        cache = new File("build/cache");
+        cache.mkdirs();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        cleanCache();
+    }
+
+    private void cleanCache() {
+        Delete del = new Delete();
+        del.setProject(new Project());
+        del.setDir(cache);
+        del.execute();
+    }
+
+    public void testNoProject() throws Exception {
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project1/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        ResolveReport report = (ResolveReport) project.getReference("ivy.resolved.report");
+        assertEquals(1, report.getDependencies().size());
+        assertEquals(MRID_MODULE1, report.getDependencies().get(0).getResolvedId());
+    }
+
+    public void testProject() throws Exception {
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project2/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        ResolveReport report = (ResolveReport) project.getReference("ivy.resolved.report");
+        assertEquals(2, report.getDependencies().size());
+        assertEquals(MRID_PROJECT1, report.getDependencies().get(0).getResolvedId());
+        assertEquals(MRID_MODULE1, report.getDependencies().get(1).getResolvedId());
+        assertEquals(1, report.getArtifactsReports(MRID_PROJECT1).length);
+        assertEquals(DownloadStatus.NO,
+            report.getArtifactsReports(MRID_PROJECT1)[0].getDownloadStatus());
+        assertEquals(new File("test/workspace/project1/target/dist/jars/project1.jar").toURI()
+                .toURL(), report.getArtifactsReports(MRID_PROJECT1)[0].getArtifact().getUrl());
+        assertEquals(
+            new File("test/workspace/project1/target/dist/jars/project1.jar").getAbsoluteFile(),
+            report.getArtifactsReports(MRID_PROJECT1)[0].getLocalFile());
+    }
+
+    public void testProjectFolder() throws Exception {
+        wa.setPath("target/classes");
+
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project2/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        ResolveReport report = (ResolveReport) project.getReference("ivy.resolved.report");
+        assertEquals(2, report.getDependencies().size());
+        assertEquals(MRID_PROJECT1, report.getDependencies().get(0).getResolvedId());
+        assertEquals(MRID_MODULE1, report.getDependencies().get(1).getResolvedId());
+        assertEquals(1, report.getArtifactsReports(MRID_PROJECT1).length);
+        assertEquals(DownloadStatus.NO,
+            report.getArtifactsReports(MRID_PROJECT1)[0].getDownloadStatus());
+        assertEquals(new File("test/workspace/project1/target/classes").toURI().toURL(),
+            report.getArtifactsReports(MRID_PROJECT1)[0].getArtifact().getUrl());
+        assertEquals(new File("test/workspace/project1/target/classes").getAbsoluteFile(),
+            report.getArtifactsReports(MRID_PROJECT1)[0].getLocalFile());
+    }
+
+    public void testDependencyArtifact() throws Exception {
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project3/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        ResolveReport report = (ResolveReport) project.getReference("ivy.resolved.report");
+        assertEquals(2, report.getDependencies().size());
+        assertEquals(MRID_PROJECT1, report.getDependencies().get(0).getResolvedId());
+        assertEquals(MRID_MODULE1, report.getDependencies().get(1).getResolvedId());
+        assertEquals(1, report.getArtifactsReports(MRID_PROJECT1).length);
+        assertEquals(DownloadStatus.NO,
+            report.getArtifactsReports(MRID_PROJECT1)[0].getDownloadStatus());
+        assertEquals(new File("test/workspace/project1/target/dist/jars/project1.jar").toURI()
+                .toURL(), report.getArtifactsReports(MRID_PROJECT1)[0].getArtifact().getUrl());
+        assertEquals(
+            new File("test/workspace/project1/target/dist/jars/project1.jar").getAbsoluteFile(),
+            report.getArtifactsReports(MRID_PROJECT1)[0].getLocalFile());
+    }
+
+    public void testCachePath() throws Exception {
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project2/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        IvyCachePath cachePath = new IvyCachePath();
+        cachePath.setProject(project);
+        cachePath.setPathid("test.cachepath.id");
+        cachePath.execute();
+
+        Path path = (Path) project.getReference("test.cachepath.id");
+        assertEquals(2, path.size());
+        assertEquals(
+            new File("test/workspace/project1/target/dist/jars/project1.jar").getAbsolutePath(),
+            path.list()[0]);
+        assertEquals(new File(cache, "org.acme/module1/jars/module1-1.1.jar").getAbsolutePath(),
+            path.list()[1]);
+    }
+
+    public void testCachePathFolder() throws Exception {
+        wa.setPath("target/classes");
+
+        IvyResolve resolve = new IvyResolve();
+        resolve.setProject(project);
+        resolve.setFile(new File("test/workspace/project2/ivy.xml"));
+        resolve.setKeep(true);
+        resolve.execute();
+
+        IvyCachePath cachePath = new IvyCachePath();
+        cachePath.setProject(project);
+        cachePath.setPathid("test.cachepath.id");
+        cachePath.execute();
+
+        Path path = (Path) project.getReference("test.cachepath.id");
+        assertEquals(2, path.size());
+        assertEquals(new File("test/workspace/project1/target/classes").getAbsolutePath(),
+            path.list()[0]);
+        assertEquals(new File(cache, "org.acme/module1/jars/module1-1.1.jar").getAbsolutePath(),
+            path.list()[1]);
+    }
+}
diff --git a/test/workspace/ivysettings.xml b/test/workspace/ivysettings.xml
new file mode 100644
index 0000000..26ebeaa
--- /dev/null
+++ b/test/workspace/ivysettings.xml
@@ -0,0 +1,30 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivysettings>
+
+    <settings defaultResolver="repo" />
+
+    <resolvers>
+        <filesystem name="repo">
+            <ivy pattern="${ivy.settings.dir}/repo/[organisation]/[module]/[revision]/[artifact].[ext]" />
+            <artifact pattern="${ivy.settings.dir}/repo/[organisation]/[module]/[revision]/[artifact].[ext]" />
+        </filesystem>
+    </resolvers>
+
+</ivysettings>
\ No newline at end of file
diff --git a/test/workspace/project1/ivy.xml b/test/workspace/project1/ivy.xml
new file mode 100644
index 0000000..bc8cc54
--- /dev/null
+++ b/test/workspace/project1/ivy.xml
@@ -0,0 +1,24 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivy-module version="2.0">
+    <info organisation="org.apache.ivy.test" module="project1" revision="0.1" />
+    <dependencies>
+        <dependency org="org.acme" name="module1" rev="1.1" />
+    </dependencies>
+</ivy-module>
\ No newline at end of file
diff --git a/test/workspace/project2/ivy.xml b/test/workspace/project2/ivy.xml
new file mode 100644
index 0000000..d995e6d
--- /dev/null
+++ b/test/workspace/project2/ivy.xml
@@ -0,0 +1,24 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivy-module version="2.0">
+    <info organisation="org.apache.ivy.test" module="project2" />
+    <dependencies>
+        <dependency org="org.apache.ivy.test" name="project1" rev="latest.integration" />
+    </dependencies>
+</ivy-module>
\ No newline at end of file
diff --git a/test/workspace/project3/ivy.xml b/test/workspace/project3/ivy.xml
new file mode 100644
index 0000000..82441dd
--- /dev/null
+++ b/test/workspace/project3/ivy.xml
@@ -0,0 +1,26 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivy-module version="2.0">
+    <info organisation="org.apache.ivy.test" module="project3" />
+    <dependencies>
+        <dependency org="org.apache.ivy.test" name="project1" rev="latest.integration">
+            <artifact name="test" type="jar" ext="jar" />
+        </dependency>
+    </dependencies>
+</ivy-module>
\ No newline at end of file
diff --git a/test/workspace/repo/org.acme/module1/1.1/ivy.xml b/test/workspace/repo/org.acme/module1/1.1/ivy.xml
new file mode 100644
index 0000000..754cc1f
--- /dev/null
+++ b/test/workspace/repo/org.acme/module1/1.1/ivy.xml
@@ -0,0 +1,21 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivy-module version="2.0">
+    <info organisation="org.acme" module="module1" revision="1.1" />
+</ivy-module>
\ No newline at end of file
diff --git a/test/workspace/repo/org.acme/module1/1.1/module1.jar b/test/workspace/repo/org.acme/module1/1.1/module1.jar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/workspace/repo/org.acme/module1/1.1/module1.jar
diff --git a/test/workspace/repo/org.acme/module2/1.2/ivy.xml b/test/workspace/repo/org.acme/module2/1.2/ivy.xml
new file mode 100644
index 0000000..990acfb
--- /dev/null
+++ b/test/workspace/repo/org.acme/module2/1.2/ivy.xml
@@ -0,0 +1,21 @@
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.    
+-->
+<ivy-module version="2.0">
+    <info organisation="org.acme" module="module2" revision="1.2" />
+</ivy-module>
\ No newline at end of file
diff --git a/test/workspace/repo/org.acme/module2/1.2/module2.jar b/test/workspace/repo/org.acme/module2/1.2/module2.jar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/workspace/repo/org.acme/module2/1.2/module2.jar