Merging demos from branch into separate folder

Signed-off-by: Manfred Moser <manfred@simpligility.com>
Signed-off-by: Manfred Moser <mmoser@apache.org>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0e36d40
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+target/
+.project
+.classpath
+.settings/
+.idea
+*.iml
+*.ipr
+*.iws
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..95d2967
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+properties([buildDiscarder(logRotator(artifactNumToKeepStr: '5', numToKeepStr: env.BRANCH_NAME=='master'?'10':'5'))])
+
+node('ubuntu') {
+    try {
+        stage('Checkout') 
+            def MAVEN_BUILD=tool name: 'Maven 3.3.9', type: 'hudson.tasks.Maven$MavenInstallation'
+            echo "Driving build and unit tests using Maven $MAVEN_BUILD"
+            def JAVA7_HOME=tool name: 'JDK 1.7 (latest)', type: 'hudson.model.JDK'
+            echo "Running build and unit tests with Java $JAVA7_HOME"
+            dir('build') {
+                deleteDir()
+            }
+            dir('build') {
+                checkout scm
+            }
+        stage('Build/Test')
+            def WORK_DIR=pwd()
+            dir('build') {
+                try {
+                    withEnv(["PATH+MAVEN=$MAVEN_BUILD/bin","PATH+JDK=$JAVA7_HOME/bin"]) {
+                        sh "mvn clean verify -B -U -e -fae -V -Dmaven.test.failure.ignore=true -Dmaven.repo.local=$WORK_DIR/.repository"
+                    }
+                } finally {
+                    junit allowEmptyResults: true, testResults:'**/target/*-reports/*.xml'
+                    archiveArtifacts allowEmptyArchive: true, artifacts: '**/target/rat.txt'
+                }
+            }
+    } finally {
+        emailext body: "See ${env.BUILD_URL}", recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'FailingTestSuspectsRecipientProvider'], [$class: 'FirstFailingBuildSuspectsRecipientProvider']], replyTo: 'dev@maven.apache.org', subject: "Maven Resolver Jenkinsfile finished with ${currentBuild.result}", to: 'notifications@maven.apache.org'
+    }
+}
diff --git a/class-overview.svg b/class-overview.svg
new file mode 100644
index 0000000..278efb2
--- /dev/null
+++ b/class-overview.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<svg xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1648" height="1517"><defs></defs><rect stroke="#000000" stroke-width="1" fill="#ffffff" x="0" y="0" width="1647" height="1516"/><g transform='translate(36.0,25.0) rotate(0 100.0 91.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='141.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='141.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >RepositorySystem</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >resolveVersionRange()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >resolveVersion()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >readArtifactDescriptor()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >collectDependencies()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='57.2'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >resolveDependencies()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='72.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >resolveArtifacts()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='86.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >resolveMetadata()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='101.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >install()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='116.39999999999999'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >deploy()</tspan></tspan></text></g></g><g transform='translate(26.0,790.0) rotate(0 100.0 61.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='81.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='81.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,106.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,102.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >Repos</tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >itorySystemSession</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >offline : boolean</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >systemProperties : Map</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >updatePolicy : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >checksumPolicy : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='57.2'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >...</tspan></tspan></text></g><g ></g></g><g transform='translate(635.3578854796845,337.77615933375705) rotate(0 69.6421145203155 37.22384066624295)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='33.4476813324859'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='138.72931179345719' height='33.4476813324859'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='69.6421145203155' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >Authentication</tspan></tspan></text></g><g ></g><g ></g></g><g transform='translate(640.0,463.5) rotate(0 70.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='139.4422310756972' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='70.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >Proxy</tspan></tspan></text></g><g ></g><g ></g></g><g transform='translate(320.0,463.5) rotate(0 100.0 38.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >ProxySelector</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getProxy()</tspan></tspan></text></g></g><g transform='translate(320.0,337.0) rotate(0 100.0 38.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >AuthenticationSelector</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getAuthentication()</tspan></tspan></text></g></g><g transform='translate(325.0,583.5) rotate(0 100.0 38.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >MirrorSelector</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getMirror()</tspan></tspan></text></g></g><g transform='translate(1130.0,248.5) rotate(0 75.0 47.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='36.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='36.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,61.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='38.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,57.0)'><rect x='0.0' y='0.0' width='149.402390438247' height='38.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='75.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >ArtifactRepsository</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >id : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >contentType : String</tspan></tspan></text></g><g ></g></g><g transform='translate(1105.0,403.5) rotate(0 100.0 38.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,46.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,42.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >LocalRepository</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >basedir : File</tspan></tspan></text></g><g ></g></g><g transform='translate(1345.0,403.5) rotate(0 100.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >WorkspaceRepository</tspan></tspan></text></g><g ></g><g ></g></g><g transform='translate(865.0,403.5) rotate(0 100.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,46.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='33.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,42.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='33.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >RemoteRepository</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >url : String</tspan></tspan></text></g><g ></g></g><g transform='translate(1205.0,404.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-60.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,-60.0) rotate(90.0)'><polygon points='0,0 12,7 12,-7' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-30.0)'></g></g><g transform='translate(1005.0,404.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-20.0' x2='170.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='170.0' y1='-20.0' x2='170.0' y2='-60.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(170.0,-60.0) rotate(90.0)'><polygon points='0,0 12,7 12,-7' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(85.0,-20.0)'></g></g><g transform='translate(1405.0,404.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-20.0' x2='-166.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-166.0' y1='-20.0' x2='-166.0' y2='-57.5' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(-166.0,-57.5) rotate(90.0)'><polygon points='0,0 12,7 12,-7' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(-83.0,-20.0)'></g></g><g transform='translate(1105.0,543.5) rotate(0 100.0 53.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >LocalRepositoryManager</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getPathForLocalArtifact()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getPathForRemoteArtifact()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getPathForLocalMetadata()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >getPathForRemoteMetadata()</tspan></tspan></text></g></g><g transform='translate(1348.0,543.5) rotate(0 100.0 38.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >WorkspaceReader</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >findArtifact()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >findVersions</tspan><tspan font-size='12' font-family='Arial' fill='#000000' >()</tspan></tspan></text></g></g><g transform='translate(866.0,545.0) rotate(0 100.0 48.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='55.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='55.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >RepositoryConnector</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >get()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >put()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >close()</tspan></tspan></text></g></g><g transform='translate(965.0,479.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='66.5' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(90.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,33.25)'></g></g><g transform='translate(1445.0,479.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='65.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(90.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,32.5)'></g></g><g transform='translate(1205.0,481.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='63.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(90.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,31.5)'></g></g><g transform='translate(865.0,426.0)'><line x1='0.0' y1='0.0' x2='-70.3578854796845' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-70.3578854796845' y1='0.0' x2='-70.3578854796845' y2='-36.1104637335028' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-70.3578854796845' y1='-36.1104637335028' x2='-90.3578854796845' y2='-36.1104637335028' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(180.0)'><polygon points='0,0 8,6 16,0 8,-6' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(-35.17894273984225,0.0)'></g></g><g transform='translate(865.0,456.0)'><line x1='0.0' y1='0.0' x2='-65.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-65.0' y1='0.0' x2='-65.0' y2='29.9999999999999' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-65.0' y1='29.9999999999999' x2='-85.0' y2='29.9999999999999' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(180.0)'><polygon points='0,0 8,6 16,0 8,-6' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(-32.5,0.0)'></g></g><g transform='translate(520.0,502.0)'><line x1='0.0' y1='0.0' x2='100.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='100.0' y1='0.0' x2='100.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='100.0' y1='0.0' x2='120.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(120.0,0.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(50.0,0.0)'></g></g><g transform='translate(520.0,375.0)'><line x1='0.0' y1='0.0' x2='115.357885479684' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(115.357885479684,0.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(57.678942739842,0.0)'></g></g><g transform='translate(865.0,703.0) rotate(0 100.0 38.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,46.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,42.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >RepositoryConnectorFactory</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >priority : int</tspan></tspan></text></g><g ></g></g><g transform='translate(965.0,703.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-62.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,-62.0) rotate(90.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-31.0)'></g></g><g transform='translate(326.0,705.5) rotate(0 100.0 53.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >TransferListener</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >transferStarted()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >transferProgressed()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >transferFailed()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >...</tspan></tspan></text></g></g><g transform='translate(330.0,845.5) rotate(0 100.0 53.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='66.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >RepositoryListener</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >artifactResolving()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >artifactResolved()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >artifactDeployed()</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >...</tspan></tspan></text></g></g><g transform='translate(336.0,1340.0) rotate(0 100.0 38.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='35.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >DependencyGraphTransformer</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >transformGraph()</tspan></tspan></text></g></g><g transform='translate(331.0,990.5) rotate(0 100.0 38.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='36.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >DependencySelector</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >selectDependency() : boolean</tspan></tspan></text></g></g><g transform='translate(336.0,1220.5) rotate(0 100.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >DependencyTraverser</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >traverseDependency() : boolean</tspan></tspan></text></g></g><g transform='translate(331.5,1105.5) rotate(0 102.5 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='204.18326693227093' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='102.5' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >DependencyManager</tspan></tspan></text></g><g ></g><g  transform='translate(0.0,58.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >manageDependency()</tspan></tspan></text></g></g><g transform='translate(896.0,1298.5) rotate(0 100.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,45.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,41.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='34.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >DependencyNode</tspan></tspan></text></g><g ></g><g ></g></g><g transform='translate(896.0,1148.5) rotate(0 100.0 37.5)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,46.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='33.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,42.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='33.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >Dependency</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >scope : String</tspan></tspan></text></g><g ></g></g><g transform='translate(893.0,928.0) rotate(0 100.0 76.0)'><g transform='translate(4.0,4.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,0.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='21.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,25.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='111.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,21.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='111.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g transform='translate(4.0,136.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#A8A8A8" stroke-width="2.0" stroke-opacity=".6" fill-opacity=".6" fill="#A8A8A8"/></g><g transform='translate(0.0,132.0)'><rect x='0.0' y='0.0' width='199.203187250996' height='20.0'  stroke="#000000" stroke-width="2.0" fill="#ffffff"/></g><g  transform='translate(0.0,15.5)'><text text-rendering='auto' font-size='12px' x='100.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' font-weight='bold' >Artifact</tspan></tspan></text></g><g  transform='translate(0.0,38.0)'><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >groupId : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >artifactId : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >extension : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >classifier : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='57.2'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >version : String</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='72.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >file : File</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='5' style='text-anchor: start' y='86.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >properties : Map</tspan></tspan></text></g><g ></g></g><g transform='translate(996.0,1149.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-68.5' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(-90.0)'><polygon points='0,0 8,6 16,0 8,-6' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-34.25)'></g></g><g transform='translate(996.0,1299.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-75.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(0.0,0.0) rotate(-90.0)'><polygon points='0,0 8,6 16,0 8,-6' fill='#FFFFFF' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-37.5)'></g></g><g transform='translate(866.0,593.0)'><line x1='0.0' y1='0.0' x2='-167.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-167.0' y1='0.0' x2='-167.0' y2='144.6' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-167.0' y1='144.6' x2='-340.0' y2='144.6' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(-340.0,144.6) rotate(360.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(-167.0,72.3)'></g></g><g transform='translate(1036.0,1299.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-20.0' x2='80.0' y2='-20.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='80.0' y1='-20.0' x2='80.0' y2='22.5' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='80.0' y1='22.5' x2='60.0' y2='22.5' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(60.0,22.5) rotate(360.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(40.0,-20.0)'></g></g><g transform='translate(586.0,1349.75) rotate(0 50.0 31.25)'><polygon points='0.0,0.0 85.65737051792827,0.0 99.601593625498,14.48675496688742 99.601593625498,62.08609271523179 0.0,62.08609271523179 '  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='0.0' x2='85.65737051792827' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='14.48675496688742' x2='99.601593625498' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><g  transform='translate(0.0,13.25)'><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >Scope</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' > Inheritance,</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' > Conflict</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='42.400000000000006'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' > Resolution</tspan></tspan></text></g></g><g transform='translate(581.0,1226.75) rotate(0 50.0 31.25)'><polygon points='0.0,0.0 85.65737051792827,0.0 99.601593625498,14.48675496688742 99.601593625498,62.08609271523179 0.0,62.08609271523179 '  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='0.0' x2='85.65737051792827' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='14.48675496688742' x2='99.601593625498' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><g  transform='translate(0.0,36.25)'><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >Fat WARs</tspan></tspan></text></g></g><g transform='translate(571.0,1000.75) rotate(0 50.0 31.25)'><polygon points='0.0,0.0 85.65737051792827,0.0 99.601593625498,14.48675496688742 99.601593625498,62.08609271523179 0.0,62.08609271523179 '  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='0.0' x2='85.65737051792827' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><line x1='85.65737051792827' y1='14.48675496688742' x2='99.601593625498' y2='14.48675496688742'  stroke="#000000" stroke-width="1.0" fill="#ffffff"/><g  transform='translate(0.0,21.25)'><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='-2.0'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' >Exclusions,</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='12.8'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' > Optional</tspan></tspan></text><text text-rendering='auto' font-size='12px' x='50.0' style='text-anchor: middle' y='27.6'><tspan><tspan font-size='12' font-family='Arial' fill='#000000' > Dependencies</tspan></tspan></text></g></g><g transform='translate(136.0,207.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='83.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='83.0' x2='-50.0' y2='83.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='-50.0' y1='83.0' x2='-50.0' y2='583.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(-50.0,583.0) rotate(270.0)'><line x1='0' y1='0' x2='12' y2='-5' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='12' y2='5' stroke='#000000' stroke-width='1' /></g><g transform='translate(-50.0,250.0)'></g></g><g transform='translate(126.0,790.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-415.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-415.0' x2='194.0' y2='-415.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(194.0,-415.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-207.5)'></g></g><g transform='translate(166.0,790.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-288.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-288.0' x2='154.0' y2='-288.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(154.0,-288.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-144.0)'></g></g><g transform='translate(166.0,790.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='-167.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='-167.0' x2='139.0' y2='-167.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='139.0' y1='-167.0' x2='139.0' y2='-168.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='139.0' y1='-168.0' x2='159.0' y2='-168.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(159.0,-168.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,-83.5)'></g></g><g transform='translate(226.0,826.6)'><line x1='0.0' y1='0.0' x2='20.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='20.0' y1='0.0' x2='20.0' y2='-88.8' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='20.0' y1='-88.8' x2='100.0' y2='-88.8' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(100.0,-88.8) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(20.0,-44.4)'></g></g><g transform='translate(226.0,851.0)'><line x1='0.0' y1='0.0' x2='84.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='84.0' y1='0.0' x2='84.0' y2='48.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='84.0' y1='48.0' x2='104.0' y2='48.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(104.0,48.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(42.0,0.0)'></g></g><g transform='translate(226.0,875.4)'><line x1='0.0' y1='0.0' x2='20.0' y2='0.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='20.0' y1='0.0' x2='20.0' y2='153.4' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='20.0' y1='153.4' x2='105.0' y2='153.4' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(105.0,153.4) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(20.0,76.7)'></g></g><g transform='translate(166.0,912.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='231.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='231.0' x2='165.5' y2='231.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(165.5,231.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,115.5)'></g></g><g transform='translate(126.0,912.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='346.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='346.0' x2='210.0' y2='346.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(210.0,346.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,173.0)'></g></g><g transform='translate(86.0,912.0)'><line x1='0.0' y1='0.0' x2='0.0' y2='466.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><line x1='0.0' y1='466.0' x2='250.0' y2='466.0' stroke-dasharray='23, 0' stroke='#000000' stroke-width='1' /><g transform='translate(250.0,466.0) rotate(180.0)'><line x1='0' y1='0' x2='7.0' y2='-3' stroke='#000000' stroke-width='1' /><line x1='0' y1='0' x2='7.0' y2='3' stroke='#000000' stroke-width='1' /></g><g transform='translate(0.0,233.0)'></g></g></svg>
diff --git a/maven-resolver-api/pom.xml b/maven-resolver-api/pom.xml
new file mode 100644
index 0000000..f67c6a9
--- /dev/null
+++ b/maven-resolver-api/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-api</artifactId>
+
+  <name>Maven Artifact Resolver API</name>
+  <description>
+    The application programming interface for the repository system.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
new file mode 100644
index 0000000..20df431
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractForwardingRepositorySystemSession.java
@@ -0,0 +1,189 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A special repository system session to enable decorating or proxying another session. To do so, clients have to
+ * create a subclass and implement {@link #getSession()}.
+ */
+public abstract class AbstractForwardingRepositorySystemSession
+    implements RepositorySystemSession
+{
+
+    /**
+     * Creates a new forwarding session.
+     */
+    protected AbstractForwardingRepositorySystemSession()
+    {
+    }
+
+    /**
+     * Gets the repository system session to which this instance forwards calls. It's worth noting that this class does
+     * not save/cache the returned reference but queries this method before each forwarding. Hence, the session
+     * forwarded to may change over time or depending on the context (e.g. calling thread).
+     * 
+     * @return The repository system session to forward calls to, never {@code null}.
+     */
+    protected abstract RepositorySystemSession getSession();
+
+    public boolean isOffline()
+    {
+        return getSession().isOffline();
+    }
+
+    public boolean isIgnoreArtifactDescriptorRepositories()
+    {
+        return getSession().isIgnoreArtifactDescriptorRepositories();
+    }
+
+    public ResolutionErrorPolicy getResolutionErrorPolicy()
+    {
+        return getSession().getResolutionErrorPolicy();
+    }
+
+    public ArtifactDescriptorPolicy getArtifactDescriptorPolicy()
+    {
+        return getSession().getArtifactDescriptorPolicy();
+    }
+
+    public String getChecksumPolicy()
+    {
+        return getSession().getChecksumPolicy();
+    }
+
+    public String getUpdatePolicy()
+    {
+        return getSession().getUpdatePolicy();
+    }
+
+    public LocalRepository getLocalRepository()
+    {
+        return getSession().getLocalRepository();
+    }
+
+    public LocalRepositoryManager getLocalRepositoryManager()
+    {
+        return getSession().getLocalRepositoryManager();
+    }
+
+    public WorkspaceReader getWorkspaceReader()
+    {
+        return getSession().getWorkspaceReader();
+    }
+
+    public RepositoryListener getRepositoryListener()
+    {
+        return getSession().getRepositoryListener();
+    }
+
+    public TransferListener getTransferListener()
+    {
+        return getSession().getTransferListener();
+    }
+
+    public Map<String, String> getSystemProperties()
+    {
+        return getSession().getSystemProperties();
+    }
+
+    public Map<String, String> getUserProperties()
+    {
+        return getSession().getUserProperties();
+    }
+
+    public Map<String, Object> getConfigProperties()
+    {
+        return getSession().getConfigProperties();
+    }
+
+    public MirrorSelector getMirrorSelector()
+    {
+        return getSession().getMirrorSelector();
+    }
+
+    public ProxySelector getProxySelector()
+    {
+        return getSession().getProxySelector();
+    }
+
+    public AuthenticationSelector getAuthenticationSelector()
+    {
+        return getSession().getAuthenticationSelector();
+    }
+
+    public ArtifactTypeRegistry getArtifactTypeRegistry()
+    {
+        return getSession().getArtifactTypeRegistry();
+    }
+
+    public DependencyTraverser getDependencyTraverser()
+    {
+        return getSession().getDependencyTraverser();
+    }
+
+    public DependencyManager getDependencyManager()
+    {
+        return getSession().getDependencyManager();
+    }
+
+    public DependencySelector getDependencySelector()
+    {
+        return getSession().getDependencySelector();
+    }
+
+    public VersionFilter getVersionFilter()
+    {
+        return getSession().getVersionFilter();
+    }
+
+    public DependencyGraphTransformer getDependencyGraphTransformer()
+    {
+        return getSession().getDependencyGraphTransformer();
+    }
+
+    public SessionData getData()
+    {
+        return getSession().getData();
+    }
+
+    public RepositoryCache getCache()
+    {
+        return getSession().getCache();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractRepositoryListener.java b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractRepositoryListener.java
new file mode 100644
index 0000000..f42d15e
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/AbstractRepositoryListener.java
@@ -0,0 +1,112 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * A skeleton implementation for custom repository listeners. The callback methods in this class do nothing.
+ */
+public abstract class AbstractRepositoryListener
+    implements RepositoryListener
+{
+
+    /**
+     * Enables subclassing.
+     */
+    protected AbstractRepositoryListener()
+    {
+    }
+
+    public void artifactDeployed( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDeploying( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDescriptorInvalid( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDescriptorMissing( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDownloaded( RepositoryEvent event )
+    {
+    }
+
+    public void artifactDownloading( RepositoryEvent event )
+    {
+    }
+
+    public void artifactInstalled( RepositoryEvent event )
+    {
+    }
+
+    public void artifactInstalling( RepositoryEvent event )
+    {
+    }
+
+    public void artifactResolved( RepositoryEvent event )
+    {
+    }
+
+    public void artifactResolving( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDeployed( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDeploying( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDownloaded( RepositoryEvent event )
+    {
+    }
+
+    public void metadataDownloading( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInstalled( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInstalling( RepositoryEvent event )
+    {
+    }
+
+    public void metadataInvalid( RepositoryEvent event )
+    {
+    }
+
+    public void metadataResolved( RepositoryEvent event )
+    {
+    }
+
+    public void metadataResolving( RepositoryEvent event )
+    {
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
new file mode 100644
index 0000000..bc1738f
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
@@ -0,0 +1,152 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * The keys and defaults for common configuration properties.
+ * 
+ * @see RepositorySystemSession#getConfigProperties()
+ */
+public final class ConfigurationProperties
+{
+
+    private static final String PREFIX_AETHER = "aether.";
+
+    private static final String PREFIX_CONNECTOR = PREFIX_AETHER + "connector.";
+
+    /**
+     * The prefix for properties that control the priority of pluggable extensions like transporters. For example, for
+     * an extension with the fully qualified class name "org.eclipse.MyExtensionFactory", the configuration properties
+     * "aether.priority.org.eclipse.MyExtensionFactory", "aether.priority.MyExtensionFactory" and
+     * "aether.priority.MyExtension" will be consulted for the priority, in that order (obviously, the last key is only
+     * tried if the class name ends with "Factory"). The corresponding value is a float and the special value
+     * {@link Float#NaN} or "NaN" (case-sensitive) can be used to disable the extension.
+     */
+    public static final String PREFIX_PRIORITY = PREFIX_AETHER + "priority.";
+
+    /**
+     * A flag indicating whether the priorities of pluggable extensions are implicitly given by their iteration order
+     * such that the first extension has the highest priority. If set, an extension's built-in priority as well as any
+     * corresponding {@code aether.priority.*} configuration properties are ignored when searching for a suitable
+     * implementation among the available extensions. This priority mode is meant for cases where the application will
+     * present/inject extensions in the desired search order.
+     * 
+     * @see #DEFAULT_IMPLICIT_PRIORITIES
+     */
+    public static final String IMPLICIT_PRIORITIES = PREFIX_PRIORITY + "implicit";
+
+    /**
+     * The default extension priority mode if {@link #IMPLICIT_PRIORITIES} isn't set.
+     */
+    public static final boolean DEFAULT_IMPLICIT_PRIORITIES = false;
+
+    /**
+     * A flag indicating whether interaction with the user is allowed.
+     * 
+     * @see #DEFAULT_INTERACTIVE
+     */
+    public static final String INTERACTIVE = PREFIX_AETHER + "interactive";
+
+    /**
+     * The default interactive mode if {@link #INTERACTIVE} isn't set.
+     */
+    public static final boolean DEFAULT_INTERACTIVE = false;
+
+    /**
+     * The user agent that repository connectors should report to servers.
+     * 
+     * @see #DEFAULT_USER_AGENT
+     */
+    public static final String USER_AGENT = PREFIX_CONNECTOR + "userAgent";
+
+    /**
+     * The default user agent to use if {@link #USER_AGENT} isn't set.
+     */
+    public static final String DEFAULT_USER_AGENT = "Aether";
+
+    /**
+     * The maximum amount of time (in milliseconds) to wait for a successful connection to a remote server. Non-positive
+     * values indicate no timeout.
+     * 
+     * @see #DEFAULT_CONNECT_TIMEOUT
+     */
+    public static final String CONNECT_TIMEOUT = PREFIX_CONNECTOR + "connectTimeout";
+
+    /**
+     * The default connect timeout to use if {@link #CONNECT_TIMEOUT} isn't set.
+     */
+    public static final int DEFAULT_CONNECT_TIMEOUT = 10 * 1000;
+
+    /**
+     * The maximum amount of time (in milliseconds) to wait for remaining data to arrive from a remote server. Note that
+     * this timeout does not restrict the overall duration of a request, it only restricts the duration of inactivity
+     * between consecutive data packets. Non-positive values indicate no timeout.
+     * 
+     * @see #DEFAULT_REQUEST_TIMEOUT
+     */
+    public static final String REQUEST_TIMEOUT = PREFIX_CONNECTOR + "requestTimeout";
+
+    /**
+     * The default request timeout to use if {@link #REQUEST_TIMEOUT} isn't set.
+     */
+    public static final int DEFAULT_REQUEST_TIMEOUT = 1800 * 1000;
+
+    /**
+     * The request headers to use for HTTP-based repository connectors. The headers are specified using a
+     * {@code Map<String, String>}, mapping a header name to its value. Besides this general key, clients may also
+     * specify headers for a specific remote repository by appending the suffix {@code .<repoId>} to this key when
+     * storing the headers map. The repository-specific headers map is supposed to be complete, i.e. is not merged with
+     * the general headers map.
+     */
+    public static final String HTTP_HEADERS = PREFIX_CONNECTOR + "http.headers";
+
+    /**
+     * The encoding/charset to use when exchanging credentials with HTTP servers. Besides this general key, clients may
+     * also specify the encoding for a specific remote repository by appending the suffix {@code .<repoId>} to this key
+     * when storing the charset name.
+     * 
+     * @see #DEFAULT_HTTP_CREDENTIAL_ENCODING
+     */
+    public static final String HTTP_CREDENTIAL_ENCODING = PREFIX_CONNECTOR + "http.credentialEncoding";
+
+    /**
+     * The default encoding/charset to use if {@link #HTTP_CREDENTIAL_ENCODING} isn't set.
+     */
+    public static final String DEFAULT_HTTP_CREDENTIAL_ENCODING = "ISO-8859-1";
+
+    /**
+     * A flag indicating whether checksums which are retrieved during checksum validation should be persisted in the
+     * local filesystem next to the file they provide the checksum for.
+     * 
+     * @see #DEFAULT_PERSISTED_CHECKSUMS
+     */
+    public static final String PERSISTED_CHECKSUMS = PREFIX_CONNECTOR + "persistedChecksums";
+
+    /**
+     * The default checksum persistence mode if {@link #PERSISTED_CHECKSUMS} isn't set.
+     */
+    public static final boolean DEFAULT_PERSISTED_CHECKSUMS = true;
+
+    private ConfigurationProperties()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositoryCache.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositoryCache.java
new file mode 100644
index 0000000..b645f43
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositoryCache.java
@@ -0,0 +1,52 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A simplistic repository cache backed by a thread-safe map. The simplistic nature of this cache makes it only suitable
+ * for use with short-lived repository system sessions where pruning of cache data is not required.
+ */
+public final class DefaultRepositoryCache
+    implements RepositoryCache
+{
+
+    private final Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>( 256 );
+
+    public Object get( RepositorySystemSession session, Object key )
+    {
+        return cache.get( key );
+    }
+
+    public void put( RepositorySystemSession session, Object key, Object data )
+    {
+        if ( data != null )
+        {
+            cache.put( key, data );
+        }
+        else
+        {
+            cache.remove( key );
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java
new file mode 100644
index 0000000..13773df
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultRepositorySystemSession.java
@@ -0,0 +1,832 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A simple repository system session.
+ * <p>
+ * <strong>Note:</strong> This class is not thread-safe. It is assumed that the mutators get only called during an
+ * initialization phase and that the session itself is not changed once initialized and being used by the repository
+ * system. It is recommended to call {@link #setReadOnly()} once the session has been fully initialized to prevent
+ * accidental manipulation of it afterwards.
+ */
+public final class DefaultRepositorySystemSession
+    implements RepositorySystemSession
+{
+
+    private boolean readOnly;
+
+    private boolean offline;
+
+    private boolean ignoreArtifactDescriptorRepositories;
+
+    private ResolutionErrorPolicy resolutionErrorPolicy;
+
+    private ArtifactDescriptorPolicy artifactDescriptorPolicy;
+
+    private String checksumPolicy;
+
+    private String updatePolicy;
+
+    private LocalRepositoryManager localRepositoryManager;
+
+    private WorkspaceReader workspaceReader;
+
+    private RepositoryListener repositoryListener;
+
+    private TransferListener transferListener;
+
+    private Map<String, String> systemProperties;
+
+    private Map<String, String> systemPropertiesView;
+
+    private Map<String, String> userProperties;
+
+    private Map<String, String> userPropertiesView;
+
+    private Map<String, Object> configProperties;
+
+    private Map<String, Object> configPropertiesView;
+
+    private MirrorSelector mirrorSelector;
+
+    private ProxySelector proxySelector;
+
+    private AuthenticationSelector authenticationSelector;
+
+    private ArtifactTypeRegistry artifactTypeRegistry;
+
+    private DependencyTraverser dependencyTraverser;
+
+    private DependencyManager dependencyManager;
+
+    private DependencySelector dependencySelector;
+
+    private VersionFilter versionFilter;
+
+    private DependencyGraphTransformer dependencyGraphTransformer;
+
+    private SessionData data;
+
+    private RepositoryCache cache;
+
+    /**
+     * Creates an uninitialized session. <em>Note:</em> The new session is not ready to use, as a bare minimum,
+     * {@link #setLocalRepositoryManager(LocalRepositoryManager)} needs to be called but usually other settings also
+     * need to be customized to achieve meaningful behavior.
+     */
+    public DefaultRepositorySystemSession()
+    {
+        systemProperties = new HashMap<String, String>();
+        systemPropertiesView = Collections.unmodifiableMap( systemProperties );
+        userProperties = new HashMap<String, String>();
+        userPropertiesView = Collections.unmodifiableMap( userProperties );
+        configProperties = new HashMap<String, Object>();
+        configPropertiesView = Collections.unmodifiableMap( configProperties );
+        mirrorSelector = NullMirrorSelector.INSTANCE;
+        proxySelector = NullProxySelector.INSTANCE;
+        authenticationSelector = NullAuthenticationSelector.INSTANCE;
+        artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE;
+        data = new DefaultSessionData();
+    }
+
+    /**
+     * Creates a shallow copy of the specified session. Actually, the copy is not completely shallow, all maps holding
+     * system/user/config properties are copied as well. In other words, invoking any mutator on the new session itself
+     * has no effect on the original session. Other mutable objects like the session data and cache (if any) are not
+     * copied and will be shared with the original session unless reconfigured.
+     *
+     * @param session The session to copy, must not be {@code null}.
+     */
+    public DefaultRepositorySystemSession( RepositorySystemSession session )
+    {
+        requireNonNull( session, "repository system session cannot be null" );
+
+        setOffline( session.isOffline() );
+        setIgnoreArtifactDescriptorRepositories( session.isIgnoreArtifactDescriptorRepositories() );
+        setResolutionErrorPolicy( session.getResolutionErrorPolicy() );
+        setArtifactDescriptorPolicy( session.getArtifactDescriptorPolicy() );
+        setChecksumPolicy( session.getChecksumPolicy() );
+        setUpdatePolicy( session.getUpdatePolicy() );
+        setLocalRepositoryManager( session.getLocalRepositoryManager() );
+        setWorkspaceReader( session.getWorkspaceReader() );
+        setRepositoryListener( session.getRepositoryListener() );
+        setTransferListener( session.getTransferListener() );
+        setSystemProperties( session.getSystemProperties() );
+        setUserProperties( session.getUserProperties() );
+        setConfigProperties( session.getConfigProperties() );
+        setMirrorSelector( session.getMirrorSelector() );
+        setProxySelector( session.getProxySelector() );
+        setAuthenticationSelector( session.getAuthenticationSelector() );
+        setArtifactTypeRegistry( session.getArtifactTypeRegistry() );
+        setDependencyTraverser( session.getDependencyTraverser() );
+        setDependencyManager( session.getDependencyManager() );
+        setDependencySelector( session.getDependencySelector() );
+        setVersionFilter( session.getVersionFilter() );
+        setDependencyGraphTransformer( session.getDependencyGraphTransformer() );
+        setData( session.getData() );
+        setCache( session.getCache() );
+    }
+
+    public boolean isOffline()
+    {
+        return offline;
+    }
+
+    /**
+     * Controls whether the repository system operates in offline mode and avoids/refuses any access to remote
+     * repositories.
+     * 
+     * @param offline {@code true} if the repository system is in offline mode, {@code false} otherwise.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setOffline( boolean offline )
+    {
+        failIfReadOnly();
+        this.offline = offline;
+        return this;
+    }
+
+    public boolean isIgnoreArtifactDescriptorRepositories()
+    {
+        return ignoreArtifactDescriptorRepositories;
+    }
+
+    /**
+     * Controls whether repositories declared in artifact descriptors should be ignored during transitive dependency
+     * collection. If enabled, only the repositories originally provided with the collect request will be considered.
+     * 
+     * @param ignoreArtifactDescriptorRepositories {@code true} to ignore additional repositories from artifact
+     *            descriptors, {@code false} to merge those with the originally specified repositories.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setIgnoreArtifactDescriptorRepositories( boolean ignoreArtifactDescriptorRepositories )
+    {
+        failIfReadOnly();
+        this.ignoreArtifactDescriptorRepositories = ignoreArtifactDescriptorRepositories;
+        return this;
+    }
+
+    public ResolutionErrorPolicy getResolutionErrorPolicy()
+    {
+        return resolutionErrorPolicy;
+    }
+
+    /**
+     * Sets the policy which controls whether resolutions errors from remote repositories should be cached.
+     * 
+     * @param resolutionErrorPolicy The resolution error policy for this session, may be {@code null} if resolution
+     *            errors should generally not be cached.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setResolutionErrorPolicy( ResolutionErrorPolicy resolutionErrorPolicy )
+    {
+        failIfReadOnly();
+        this.resolutionErrorPolicy = resolutionErrorPolicy;
+        return this;
+    }
+
+    public ArtifactDescriptorPolicy getArtifactDescriptorPolicy()
+    {
+        return artifactDescriptorPolicy;
+    }
+
+    /**
+     * Sets the policy which controls how errors related to reading artifact descriptors should be handled.
+     * 
+     * @param artifactDescriptorPolicy The descriptor error policy for this session, may be {@code null} if descriptor
+     *            errors should generally not be tolerated.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setArtifactDescriptorPolicy( ArtifactDescriptorPolicy artifactDescriptorPolicy )
+    {
+        failIfReadOnly();
+        this.artifactDescriptorPolicy = artifactDescriptorPolicy;
+        return this;
+    }
+
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    /**
+     * Sets the global checksum policy. If set, the global checksum policy overrides the checksum policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @param checksumPolicy The global checksum policy, may be {@code null}/empty to apply the per-repository policies.
+     * @return This session for chaining, never {@code null}.
+     * @see RepositoryPolicy#CHECKSUM_POLICY_FAIL
+     * @see RepositoryPolicy#CHECKSUM_POLICY_IGNORE
+     * @see RepositoryPolicy#CHECKSUM_POLICY_WARN
+     */
+    public DefaultRepositorySystemSession setChecksumPolicy( String checksumPolicy )
+    {
+        failIfReadOnly();
+        this.checksumPolicy = checksumPolicy;
+        return this;
+    }
+
+    public String getUpdatePolicy()
+    {
+        return updatePolicy;
+    }
+
+    /**
+     * Sets the global update policy. If set, the global update policy overrides the update policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @param updatePolicy The global update policy, may be {@code null}/empty to apply the per-repository policies.
+     * @return This session for chaining, never {@code null}.
+     * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS
+     * @see RepositoryPolicy#UPDATE_POLICY_DAILY
+     * @see RepositoryPolicy#UPDATE_POLICY_NEVER
+     */
+    public DefaultRepositorySystemSession setUpdatePolicy( String updatePolicy )
+    {
+        failIfReadOnly();
+        this.updatePolicy = updatePolicy;
+        return this;
+    }
+
+    public LocalRepository getLocalRepository()
+    {
+        LocalRepositoryManager lrm = getLocalRepositoryManager();
+        return ( lrm != null ) ? lrm.getRepository() : null;
+    }
+
+    public LocalRepositoryManager getLocalRepositoryManager()
+    {
+        return localRepositoryManager;
+    }
+
+    /**
+     * Sets the local repository manager used during this session. <em>Note:</em> Eventually, a valid session must have
+     * a local repository manager set.
+     * 
+     * @param localRepositoryManager The local repository manager used during this session, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setLocalRepositoryManager( LocalRepositoryManager localRepositoryManager )
+    {
+        failIfReadOnly();
+        this.localRepositoryManager = localRepositoryManager;
+        return this;
+    }
+
+    public WorkspaceReader getWorkspaceReader()
+    {
+        return workspaceReader;
+    }
+
+    /**
+     * Sets the workspace reader used during this session. If set, the workspace reader will usually be consulted first
+     * to resolve artifacts.
+     * 
+     * @param workspaceReader The workspace reader for this session, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setWorkspaceReader( WorkspaceReader workspaceReader )
+    {
+        failIfReadOnly();
+        this.workspaceReader = workspaceReader;
+        return this;
+    }
+
+    public RepositoryListener getRepositoryListener()
+    {
+        return repositoryListener;
+    }
+
+    /**
+     * Sets the listener being notified of actions in the repository system.
+     * 
+     * @param repositoryListener The repository listener, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setRepositoryListener( RepositoryListener repositoryListener )
+    {
+        failIfReadOnly();
+        this.repositoryListener = repositoryListener;
+        return this;
+    }
+
+    public TransferListener getTransferListener()
+    {
+        return transferListener;
+    }
+
+    /**
+     * Sets the listener being notified of uploads/downloads by the repository system.
+     * 
+     * @param transferListener The transfer listener, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setTransferListener( TransferListener transferListener )
+    {
+        failIfReadOnly();
+        this.transferListener = transferListener;
+        return this;
+    }
+
+    private <T> Map<String, T> copySafe( Map<?, ?> table, Class<T> valueType )
+    {
+        Map<String, T> map;
+        if ( table == null || table.isEmpty() )
+        {
+            map = new HashMap<String, T>();
+        }
+        else
+        {
+            map = new HashMap<String, T>( (int) ( table.size() / 0.75f ) + 1 );
+            for ( Map.Entry<?, ?> entry : table.entrySet() )
+            {
+                Object key = entry.getKey();
+                if ( key instanceof String )
+                {
+                    Object value = entry.getValue();
+                    if ( valueType.isInstance( value ) )
+                    {
+                        map.put( key.toString(), valueType.cast( value ) );
+                    }
+                }
+            }
+        }
+        return map;
+    }
+
+    public Map<String, String> getSystemProperties()
+    {
+        return systemPropertiesView;
+    }
+
+    /**
+     * Sets the system properties to use, e.g. for processing of artifact descriptors. System properties are usually
+     * collected from the runtime environment like {@link System#getProperties()} and environment variables.
+     * <p>
+     * <em>Note:</em> System properties are of type {@code Map<String, String>} and any key-value pair in the input map
+     * that doesn't match this type will be silently ignored.
+     * 
+     * @param systemProperties The system properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setSystemProperties( Map<?, ?> systemProperties )
+    {
+        failIfReadOnly();
+        this.systemProperties = copySafe( systemProperties, String.class );
+        systemPropertiesView = Collections.unmodifiableMap( this.systemProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified system property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setSystemProperty( String key, String value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            systemProperties.put( key, value );
+        }
+        else
+        {
+            systemProperties.remove( key );
+        }
+        return this;
+    }
+
+    public Map<String, String> getUserProperties()
+    {
+        return userPropertiesView;
+    }
+
+    /**
+     * Sets the user properties to use, e.g. for processing of artifact descriptors. User properties are similar to
+     * system properties but are set on the discretion of the user and hence are considered of higher priority than
+     * system properties in case of conflicts.
+     * <p>
+     * <em>Note:</em> User properties are of type {@code Map<String, String>} and any key-value pair in the input map
+     * that doesn't match this type will be silently ignored.
+     * 
+     * @param userProperties The user properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setUserProperties( Map<?, ?> userProperties )
+    {
+        failIfReadOnly();
+        this.userProperties = copySafe( userProperties, String.class );
+        userPropertiesView = Collections.unmodifiableMap( this.userProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified user property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setUserProperty( String key, String value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            userProperties.put( key, value );
+        }
+        else
+        {
+            userProperties.remove( key );
+        }
+        return this;
+    }
+
+    public Map<String, Object> getConfigProperties()
+    {
+        return configPropertiesView;
+    }
+
+    /**
+     * Sets the configuration properties used to tweak internal aspects of the repository system (e.g. thread pooling,
+     * connector-specific behavior, etc.).
+     * <p>
+     * <em>Note:</em> Configuration properties are of type {@code Map<String, Object>} and any key-value pair in the
+     * input map that doesn't match this type will be silently ignored.
+     * 
+     * @param configProperties The configuration properties, may be {@code null} or empty if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setConfigProperties( Map<?, ?> configProperties )
+    {
+        failIfReadOnly();
+        this.configProperties = copySafe( configProperties, Object.class );
+        configPropertiesView = Collections.unmodifiableMap( this.configProperties );
+        return this;
+    }
+
+    /**
+     * Sets the specified configuration property.
+     * 
+     * @param key The property key, must not be {@code null}.
+     * @param value The property value, may be {@code null} to remove/unset the property.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setConfigProperty( String key, Object value )
+    {
+        failIfReadOnly();
+        if ( value != null )
+        {
+            configProperties.put( key, value );
+        }
+        else
+        {
+            configProperties.remove( key );
+        }
+        return this;
+    }
+
+    public MirrorSelector getMirrorSelector()
+    {
+        return mirrorSelector;
+    }
+
+    /**
+     * Sets the mirror selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to denote the effective repositories.
+     * 
+     * @param mirrorSelector The mirror selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setMirrorSelector( MirrorSelector mirrorSelector )
+    {
+        failIfReadOnly();
+        this.mirrorSelector = mirrorSelector;
+        if ( this.mirrorSelector == null )
+        {
+            this.mirrorSelector = NullMirrorSelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public ProxySelector getProxySelector()
+    {
+        return proxySelector;
+    }
+
+    /**
+     * Sets the proxy selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to have their proxy (if any) already set.
+     * 
+     * @param proxySelector The proxy selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getProxy()
+     */
+    public DefaultRepositorySystemSession setProxySelector( ProxySelector proxySelector )
+    {
+        failIfReadOnly();
+        this.proxySelector = proxySelector;
+        if ( this.proxySelector == null )
+        {
+            this.proxySelector = NullProxySelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public AuthenticationSelector getAuthenticationSelector()
+    {
+        return authenticationSelector;
+    }
+
+    /**
+     * Sets the authentication selector to use for repositories discovered in artifact descriptors. Note that this
+     * selector is not used for remote repositories which are passed as request parameters to the repository system,
+     * those repositories are supposed to have their authentication (if any) already set.
+     * 
+     * @param authenticationSelector The authentication selector to use, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getAuthentication()
+     */
+    public DefaultRepositorySystemSession setAuthenticationSelector( AuthenticationSelector authenticationSelector )
+    {
+        failIfReadOnly();
+        this.authenticationSelector = authenticationSelector;
+        if ( this.authenticationSelector == null )
+        {
+            this.authenticationSelector = NullAuthenticationSelector.INSTANCE;
+        }
+        return this;
+    }
+
+    public ArtifactTypeRegistry getArtifactTypeRegistry()
+    {
+        return artifactTypeRegistry;
+    }
+
+    /**
+     * Sets the registry of artifact types recognized by this session.
+     * 
+     * @param artifactTypeRegistry The artifact type registry, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setArtifactTypeRegistry( ArtifactTypeRegistry artifactTypeRegistry )
+    {
+        failIfReadOnly();
+        this.artifactTypeRegistry = artifactTypeRegistry;
+        if ( this.artifactTypeRegistry == null )
+        {
+            this.artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE;
+        }
+        return this;
+    }
+
+    public DependencyTraverser getDependencyTraverser()
+    {
+        return dependencyTraverser;
+    }
+
+    /**
+     * Sets the dependency traverser to use for building dependency graphs.
+     * 
+     * @param dependencyTraverser The dependency traverser to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyTraverser( DependencyTraverser dependencyTraverser )
+    {
+        failIfReadOnly();
+        this.dependencyTraverser = dependencyTraverser;
+        return this;
+    }
+
+    public DependencyManager getDependencyManager()
+    {
+        return dependencyManager;
+    }
+
+    /**
+     * Sets the dependency manager to use for building dependency graphs.
+     * 
+     * @param dependencyManager The dependency manager to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyManager( DependencyManager dependencyManager )
+    {
+        failIfReadOnly();
+        this.dependencyManager = dependencyManager;
+        return this;
+    }
+
+    public DependencySelector getDependencySelector()
+    {
+        return dependencySelector;
+    }
+
+    /**
+     * Sets the dependency selector to use for building dependency graphs.
+     * 
+     * @param dependencySelector The dependency selector to use for building dependency graphs, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencySelector( DependencySelector dependencySelector )
+    {
+        failIfReadOnly();
+        this.dependencySelector = dependencySelector;
+        return this;
+    }
+
+    public VersionFilter getVersionFilter()
+    {
+        return versionFilter;
+    }
+
+    /**
+     * Sets the version filter to use for building dependency graphs.
+     * 
+     * @param versionFilter The version filter to use for building dependency graphs, may be {@code null} to not filter
+     *            versions.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setVersionFilter( VersionFilter versionFilter )
+    {
+        failIfReadOnly();
+        this.versionFilter = versionFilter;
+        return this;
+    }
+
+    public DependencyGraphTransformer getDependencyGraphTransformer()
+    {
+        return dependencyGraphTransformer;
+    }
+
+    /**
+     * Sets the dependency graph transformer to use for building dependency graphs.
+     * 
+     * @param dependencyGraphTransformer The dependency graph transformer to use for building dependency graphs, may be
+     *            {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setDependencyGraphTransformer( DependencyGraphTransformer dependencyGraphTransformer )
+    {
+        failIfReadOnly();
+        this.dependencyGraphTransformer = dependencyGraphTransformer;
+        return this;
+    }
+
+    public SessionData getData()
+    {
+        return data;
+    }
+
+    /**
+     * Sets the custom data associated with this session.
+     * 
+     * @param data The session data, may be {@code null}.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setData( SessionData data )
+    {
+        failIfReadOnly();
+        this.data = data;
+        if ( this.data == null )
+        {
+            this.data = new DefaultSessionData();
+        }
+        return this;
+    }
+
+    public RepositoryCache getCache()
+    {
+        return cache;
+    }
+
+    /**
+     * Sets the cache the repository system may use to save data for future reuse during the session.
+     * 
+     * @param cache The repository cache, may be {@code null} if none.
+     * @return This session for chaining, never {@code null}.
+     */
+    public DefaultRepositorySystemSession setCache( RepositoryCache cache )
+    {
+        failIfReadOnly();
+        this.cache = cache;
+        return this;
+    }
+
+    /**
+     * Marks this session as read-only such that any future attempts to call its mutators will fail with an exception.
+     * Marking an already read-only session as read-only has no effect. The session's data and cache remain writable
+     * though.
+     */
+    public void setReadOnly()
+    {
+        readOnly = true;
+    }
+
+    private void failIfReadOnly()
+    {
+        if ( readOnly )
+        {
+            throw new IllegalStateException( "repository system session is read-only" );
+        }
+    }
+
+    static class NullProxySelector
+        implements ProxySelector
+    {
+
+        public static final ProxySelector INSTANCE = new NullProxySelector();
+
+        public Proxy getProxy( RemoteRepository repository )
+        {
+            return repository.getProxy();
+        }
+
+    }
+
+    static class NullMirrorSelector
+        implements MirrorSelector
+    {
+
+        public static final MirrorSelector INSTANCE = new NullMirrorSelector();
+
+        public RemoteRepository getMirror( RemoteRepository repository )
+        {
+            return null;
+        }
+
+    }
+
+    static class NullAuthenticationSelector
+        implements AuthenticationSelector
+    {
+
+        public static final AuthenticationSelector INSTANCE = new NullAuthenticationSelector();
+
+        public Authentication getAuthentication( RemoteRepository repository )
+        {
+            return repository.getAuthentication();
+        }
+
+    }
+
+    static final class NullArtifactTypeRegistry
+        implements ArtifactTypeRegistry
+    {
+
+        public static final ArtifactTypeRegistry INSTANCE = new NullArtifactTypeRegistry();
+
+        public ArtifactType get( String typeId )
+        {
+            return null;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultSessionData.java b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultSessionData.java
new file mode 100644
index 0000000..3c2a76e
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/DefaultSessionData.java
@@ -0,0 +1,84 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A simple session data storage backed by a thread-safe map.
+ */
+public final class DefaultSessionData
+    implements SessionData
+{
+
+    private final ConcurrentMap<Object, Object> data;
+
+    public DefaultSessionData()
+    {
+        data = new ConcurrentHashMap<Object, Object>();
+    }
+
+    public void set( Object key, Object value )
+    {
+        requireNonNull( key, "key cannot be null" );
+
+        if ( value != null )
+        {
+            data.put( key, value );
+        }
+        else
+        {
+            data.remove( key );
+        }
+    }
+
+    public boolean set( Object key, Object oldValue, Object newValue )
+    {
+        requireNonNull( key, "key cannot be null" );
+
+        if ( newValue != null )
+        {
+            if ( oldValue == null )
+            {
+                return data.putIfAbsent( key, newValue ) == null;
+            }
+            return data.replace( key, oldValue, newValue );
+        }
+        else
+        {
+            if ( oldValue == null )
+            {
+                return !data.containsKey( key );
+            }
+            return data.remove( key, oldValue );
+        }
+    }
+
+    public Object get( Object key )
+    {
+        requireNonNull( key, "key cannot be null" );
+
+        return data.get( key );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryCache.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryCache.java
new file mode 100644
index 0000000..6f9f114
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryCache.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * Caches auxiliary data used during repository access like already processed metadata. The data in the cache is meant
+ * for exclusive consumption by the repository system and is opaque to the cache implementation. <strong>Note:</strong>
+ * Actual cache implementations must be thread-safe.
+ * 
+ * @see RepositorySystemSession#getCache()
+ */
+public interface RepositoryCache
+{
+
+    /**
+     * Puts the specified data into the cache. It is entirely up to the cache implementation how long this data will be
+     * kept before being purged, i.e. callers must not make any assumptions about the lifetime of cached data.
+     * <p>
+     * <em>Warning:</em> The cache will directly save the provided reference. If the cached data is mutable, i.e. could
+     * be modified after being put into the cache, the caller is responsible for creating a copy of the original data
+     * and store the copy in the cache.
+     * 
+     * @param session The repository session during which the cache is accessed, must not be {@code null}.
+     * @param key The key to use for lookup of the data, must not be {@code null}.
+     * @param data The data to store in the cache, may be {@code null}.
+     */
+    void put( RepositorySystemSession session, Object key, Object data );
+
+    /**
+     * Gets the specified data from the cache.
+     * <p>
+     * <em>Warning:</em> The cache will directly return the saved reference. If the cached data is to be modified after
+     * its retrieval, the caller is responsible to create a copy of the returned data and use this instead of the cache
+     * record.
+     * 
+     * @param session The repository session during which the cache is accessed, must not be {@code null}.
+     * @param key The key to use for lookup of the data, must not be {@code null}.
+     * @return The requested data or {@code null} if none was present in the cache.
+     */
+    Object get( RepositorySystemSession session, Object key );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java
new file mode 100644
index 0000000..812ef7d
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java
@@ -0,0 +1,435 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.ArtifactRepository;
+
+/**
+ * An event describing an action performed by the repository system. Note that events which indicate the end of an
+ * action like {@link EventType#ARTIFACT_RESOLVED} are generally fired in both the success and the failure case. Use
+ * {@link #getException()} to check whether an event denotes success or failure.
+ * 
+ * @see RepositoryListener
+ * @see RepositoryEvent.Builder
+ */
+public final class RepositoryEvent
+{
+
+    /**
+     * The type of the repository event.
+     */
+    public enum EventType
+    {
+
+        /**
+         * @see RepositoryListener#artifactDescriptorInvalid(RepositoryEvent)
+         */
+        ARTIFACT_DESCRIPTOR_INVALID,
+
+        /**
+         * @see RepositoryListener#artifactDescriptorMissing(RepositoryEvent)
+         */
+        ARTIFACT_DESCRIPTOR_MISSING,
+
+        /**
+         * @see RepositoryListener#metadataInvalid(RepositoryEvent)
+         */
+        METADATA_INVALID,
+
+        /**
+         * @see RepositoryListener#artifactResolving(RepositoryEvent)
+         */
+        ARTIFACT_RESOLVING,
+
+        /**
+         * @see RepositoryListener#artifactResolved(RepositoryEvent)
+         */
+        ARTIFACT_RESOLVED,
+
+        /**
+         * @see RepositoryListener#metadataResolving(RepositoryEvent)
+         */
+        METADATA_RESOLVING,
+
+        /**
+         * @see RepositoryListener#metadataResolved(RepositoryEvent)
+         */
+        METADATA_RESOLVED,
+
+        /**
+         * @see RepositoryListener#artifactDownloading(RepositoryEvent)
+         */
+        ARTIFACT_DOWNLOADING,
+
+        /**
+         * @see RepositoryListener#artifactDownloaded(RepositoryEvent)
+         */
+        ARTIFACT_DOWNLOADED,
+
+        /**
+         * @see RepositoryListener#metadataDownloading(RepositoryEvent)
+         */
+        METADATA_DOWNLOADING,
+
+        /**
+         * @see RepositoryListener#metadataDownloaded(RepositoryEvent)
+         */
+        METADATA_DOWNLOADED,
+
+        /**
+         * @see RepositoryListener#artifactInstalling(RepositoryEvent)
+         */
+        ARTIFACT_INSTALLING,
+
+        /**
+         * @see RepositoryListener#artifactInstalled(RepositoryEvent)
+         */
+        ARTIFACT_INSTALLED,
+
+        /**
+         * @see RepositoryListener#metadataInstalling(RepositoryEvent)
+         */
+        METADATA_INSTALLING,
+
+        /**
+         * @see RepositoryListener#metadataInstalled(RepositoryEvent)
+         */
+        METADATA_INSTALLED,
+
+        /**
+         * @see RepositoryListener#artifactDeploying(RepositoryEvent)
+         */
+        ARTIFACT_DEPLOYING,
+
+        /**
+         * @see RepositoryListener#artifactDeployed(RepositoryEvent)
+         */
+        ARTIFACT_DEPLOYED,
+
+        /**
+         * @see RepositoryListener#metadataDeploying(RepositoryEvent)
+         */
+        METADATA_DEPLOYING,
+
+        /**
+         * @see RepositoryListener#metadataDeployed(RepositoryEvent)
+         */
+        METADATA_DEPLOYED
+
+    }
+
+    private final EventType type;
+
+    private final RepositorySystemSession session;
+
+    private final Artifact artifact;
+
+    private final Metadata metadata;
+
+    private final ArtifactRepository repository;
+
+    private final File file;
+
+    private final List<Exception> exceptions;
+
+    private final RequestTrace trace;
+
+    RepositoryEvent( Builder builder )
+    {
+        type = builder.type;
+        session = builder.session;
+        artifact = builder.artifact;
+        metadata = builder.metadata;
+        repository = builder.repository;
+        file = builder.file;
+        exceptions = builder.exceptions;
+        trace = builder.trace;
+    }
+
+    /**
+     * Gets the type of the event.
+     * 
+     * @return The type of the event, never {@code null}.
+     */
+    public EventType getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the repository system session during which the event occurred.
+     * 
+     * @return The repository system session during which the event occurred, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the artifact involved in the event (if any).
+     * 
+     * @return The involved artifact or {@code null} if none.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Gets the metadata involved in the event (if any).
+     * 
+     * @return The involved metadata or {@code null} if none.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Gets the file involved in the event (if any).
+     * 
+     * @return The involved file or {@code null} if none.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Gets the repository involved in the event (if any).
+     * 
+     * @return The involved repository or {@code null} if none.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the exception that caused the event (if any). As a rule of thumb, an event accompanied by an exception
+     * indicates a failure of the corresponding action. If multiple exceptions occurred, this method returns the first
+     * exception.
+     * 
+     * @return The exception or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exceptions.isEmpty() ? null : exceptions.get( 0 );
+    }
+
+    /**
+     * Gets the exceptions that caused the event (if any). As a rule of thumb, an event accompanied by exceptions
+     * indicates a failure of the corresponding action.
+     * 
+     * @return The exceptions, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Gets the trace information about the request during which the event occurred.
+     * 
+     * @return The trace information or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( getType() );
+        if ( getArtifact() != null )
+        {
+            buffer.append( " " ).append( getArtifact() );
+        }
+        if ( getMetadata() != null )
+        {
+            buffer.append( " " ).append( getMetadata() );
+        }
+        if ( getFile() != null )
+        {
+            buffer.append( " (" ).append( getFile() ).append( ")" );
+        }
+        if ( getRepository() != null )
+        {
+            buffer.append( " @ " ).append( getRepository() );
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * A builder to create events.
+     */
+    public static final class Builder
+    {
+
+        EventType type;
+
+        RepositorySystemSession session;
+
+        Artifact artifact;
+
+        Metadata metadata;
+
+        ArtifactRepository repository;
+
+        File file;
+
+        List<Exception> exceptions = Collections.emptyList();
+
+        RequestTrace trace;
+
+        /**
+         * Creates a new event builder for the specified session and event type.
+         *
+         * @param session The repository system session, must not be {@code null}.
+         * @param type The type of the event, must not be {@code null}.
+         */
+        public Builder( RepositorySystemSession session, EventType type )
+        {
+            this.session = requireNonNull( session, "session cannot be null" );
+            this.type = requireNonNull( type, "event type cannot be null" );
+        }
+
+        /**
+         * Sets the artifact involved in the event.
+         *
+         * @param artifact The involved artifact, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setArtifact( Artifact artifact )
+        {
+            this.artifact = artifact;
+            return this;
+        }
+
+        /**
+         * Sets the metadata involved in the event.
+         * 
+         * @param metadata The involved metadata, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setMetadata( Metadata metadata )
+        {
+            this.metadata = metadata;
+            return this;
+        }
+
+        /**
+         * Sets the repository involved in the event.
+         * 
+         * @param repository The involved repository, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setRepository( ArtifactRepository repository )
+        {
+            this.repository = repository;
+            return this;
+        }
+
+        /**
+         * Sets the file involved in the event.
+         * 
+         * @param file The involved file, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setFile( File file )
+        {
+            this.file = file;
+            return this;
+        }
+
+        /**
+         * Sets the exception causing the event.
+         * 
+         * @param exception The exception causing the event, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setException( Exception exception )
+        {
+            if ( exception != null )
+            {
+                this.exceptions = Collections.singletonList( exception );
+            }
+            else
+            {
+                this.exceptions = Collections.emptyList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the exceptions causing the event.
+         * 
+         * @param exceptions The exceptions causing the event, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setExceptions( List<Exception> exceptions )
+        {
+            if ( exceptions != null )
+            {
+                this.exceptions = exceptions;
+            }
+            else
+            {
+                this.exceptions = Collections.emptyList();
+            }
+            return this;
+        }
+
+        /**
+         * Sets the trace information about the request during which the event occurred.
+         * 
+         * @param trace The trace information, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setTrace( RequestTrace trace )
+        {
+            this.trace = trace;
+            return this;
+        }
+
+        /**
+         * Builds a new event from the current values of this builder. The state of the builder itself remains
+         * unchanged.
+         * 
+         * @return The event, never {@code null}.
+         */
+        public RepositoryEvent build()
+        {
+            return new RepositoryEvent( this );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryException.java
new file mode 100644
index 0000000..c2d1718
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryException.java
@@ -0,0 +1,69 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * The base class for exceptions thrown by the repository system. <em>Note:</em> Unless otherwise noted, instances of
+ * this class and its subclasses will not persist fields carrying extended error information during serialization.
+ */
+public class RepositoryException
+    extends Exception
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public RepositoryException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public RepositoryException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+    /**
+     * @noreference This method is not intended to be used by clients.
+     */
+    protected static String getMessage( String prefix, Throwable cause )
+    {
+        String msg = "";
+        if ( cause != null )
+        {
+            msg = cause.getMessage();
+            if ( msg == null || msg.length() <= 0 )
+            {
+                msg = cause.getClass().getSimpleName();
+            }
+            msg = prefix + msg;
+        }
+        return msg;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryListener.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryListener.java
new file mode 100644
index 0000000..d654630
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryListener.java
@@ -0,0 +1,222 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * A listener being notified of events from the repository system. In general, the system sends events upon termination
+ * of an operation like {@link #artifactResolved(RepositoryEvent)} regardless whether it succeeded or failed so
+ * listeners need to inspect the event details carefully. Also, the listener may be called from an arbitrary thread.
+ * <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractRepositoryListener} instead of
+ * directly implementing this interface.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getRepositoryListener()
+ * @see org.eclipse.aether.transfer.TransferListener
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositoryListener
+{
+
+    /**
+     * Notifies the listener of a syntactically or semantically invalid artifact descriptor.
+     * {@link RepositoryEvent#getArtifact()} indicates the artifact whose descriptor is invalid and
+     * {@link RepositoryEvent#getExceptions()} carries the encountered errors. Depending on the session's
+     * {@link org.eclipse.aether.resolution.ArtifactDescriptorPolicy}, the underlying repository operation might abort
+     * with an exception or ignore the invalid descriptor.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDescriptorInvalid( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of a missing artifact descriptor. {@link RepositoryEvent#getArtifact()} indicates the
+     * artifact whose descriptor is missing. Depending on the session's
+     * {@link org.eclipse.aether.resolution.ArtifactDescriptorPolicy}, the underlying repository operation might abort
+     * with an exception or ignore the missing descriptor.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDescriptorMissing( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of syntactically or semantically invalid metadata. {@link RepositoryEvent#getMetadata()}
+     * indicates the invalid metadata and {@link RepositoryEvent#getExceptions()} carries the encountered errors. The
+     * underlying repository operation might still succeed, depending on whether the metadata in question is actually
+     * needed to carry out the resolution process.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInvalid( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be resolved. {@link RepositoryEvent#getArtifact()} denotes
+     * the artifact in question. Unlike the {@link #artifactDownloading(RepositoryEvent)} event, this event is fired
+     * regardless whether the artifact already exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactResolving( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose resolution has been completed, either successfully or not.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the resolution succeeded or failed. Unlike the
+     * {@link #artifactDownloaded(RepositoryEvent)} event, this event is fired regardless whether the artifact already
+     * exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactResolved( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be resolved. {@link RepositoryEvent#getMetadata()}
+     * denotes the metadata in question. Unlike the {@link #metadataDownloading(RepositoryEvent)} event, this event is
+     * fired regardless whether the metadata already exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataResolving( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose resolution has been completed, either successfully or not.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the resolution succeeded or failed. Unlike the
+     * {@link #metadataDownloaded(RepositoryEvent)} event, this event is fired regardless whether the metadata already
+     * exists locally or not.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataResolved( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be downloaded from a remote repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getRepository()} the source repository. Unlike the
+     * {@link #artifactResolving(RepositoryEvent)} event, this event is only fired when the artifact does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDownloading( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose download has been completed, either successfully or not.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the download succeeded or failed. Unlike the
+     * {@link #artifactResolved(RepositoryEvent)} event, this event is only fired when the artifact does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDownloaded( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be downloaded from a remote repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getRepository()} the source repository. Unlike the
+     * {@link #metadataResolving(RepositoryEvent)} event, this event is only fired when the metadata does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDownloading( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose download has been completed, either successfully or not.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the download succeeded or failed. Unlike the
+     * {@link #metadataResolved(RepositoryEvent)} event, this event is only fired when the metadata does not already
+     * exist locally.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDownloaded( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be installed to the local repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactInstalling( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose installation to the local repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the installation succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactInstalled( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be installed to the local repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInstalling( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose installation to the local repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the installation succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataInstalled( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact that is about to be uploaded to a remote repository.
+     * {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getRepository()} the destination repository.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDeploying( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of an artifact whose upload to a remote repository has been completed, either successfully
+     * or not. {@link RepositoryEvent#getArtifact()} denotes the artifact in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the upload succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void artifactDeployed( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata that is about to be uploaded to a remote repository.
+     * {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getRepository()} the destination repository.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDeploying( RepositoryEvent event );
+
+    /**
+     * Notifies the listener of some metadata whose upload to a remote repository has been completed, either
+     * successfully or not. {@link RepositoryEvent#getMetadata()} denotes the metadata in question and
+     * {@link RepositoryEvent#getExceptions()} indicates whether the upload succeeded or failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void metadataDeployed( RepositoryEvent event );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystem.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystem.java
new file mode 100644
index 0000000..8706f89
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystem.java
@@ -0,0 +1,277 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+
+/**
+ * The main entry point to the repository system and its functionality. Note that obtaining a concrete implementation of
+ * this interface (e.g. via dependency injection, service locator, etc.) is dependent on the application and its
+ * specific needs, please consult the online documentation for examples and directions on booting the system.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositorySystem
+{
+
+    /**
+     * Expands a version range to a list of matching versions, in ascending order. For example, resolves "[3.8,4.0)" to
+     * "3.8", "3.8.1", "3.8.2". Note that the returned list of versions is only dependent on the configured repositories
+     * and their contents, the list is not processed by the {@link RepositorySystemSession#getVersionFilter() session's
+     * version filter}.
+     * <p>
+     * The supplied request may also refer to a single concrete version rather than a version range. In this case
+     * though, the result contains simply the (parsed) input version, regardless of the repositories and their contents.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version range request, must not be {@code null}.
+     * @return The version range result, never {@code null}.
+     * @throws VersionRangeResolutionException If the requested range could not be parsed. Note that an empty range does
+     *             not raise an exception.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException;
+
+    /**
+     * Resolves an artifact's meta version (if any) to a concrete version. For example, resolves "1.0-SNAPSHOT" to
+     * "1.0-20090208.132618-23".
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version request, must not be {@code null}.
+     * @return The version result, never {@code null}.
+     * @throws VersionResolutionException If the metaversion could not be resolved.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException;
+
+    /**
+     * Gets information about an artifact like its direct dependencies and potential relocations.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The descriptor request, must not be {@code null}.
+     * @return The descriptor result, never {@code null}.
+     * @throws ArtifactDescriptorException If the artifact descriptor could not be read.
+     * @see RepositorySystemSession#getArtifactDescriptorPolicy()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session, ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException;
+
+    /**
+     * Collects the transitive dependencies of an artifact and builds a dependency graph. Note that this operation is
+     * only concerned about determining the coordinates of the transitive dependencies. To also resolve the actual
+     * artifact files, use {@link #resolveDependencies(RepositorySystemSession, DependencyRequest)}.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The collection request, must not be {@code null}.
+     * @return The collection result, never {@code null}.
+     * @throws DependencyCollectionException If the dependency tree could not be built.
+     * @see RepositorySystemSession#getDependencyTraverser()
+     * @see RepositorySystemSession#getDependencyManager()
+     * @see RepositorySystemSession#getDependencySelector()
+     * @see RepositorySystemSession#getVersionFilter()
+     * @see RepositorySystemSession#getDependencyGraphTransformer()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
+        throws DependencyCollectionException;
+
+    /**
+     * Collects and resolves the transitive dependencies of an artifact. This operation is essentially a combination of
+     * {@link #collectDependencies(RepositorySystemSession, CollectRequest)} and
+     * {@link #resolveArtifacts(RepositorySystemSession, Collection)}.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The dependency request, must not be {@code null}.
+     * @return The dependency result, never {@code null}.
+     * @throws DependencyResolutionException If the dependency tree could not be built or any dependency artifact could
+     *             not be resolved.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    DependencyResult resolveDependencies( RepositorySystemSession session, DependencyRequest request )
+        throws DependencyResolutionException;
+
+    /**
+     * Resolves the path for an artifact. The artifact will be downloaded to the local repository if necessary. An
+     * artifact that is already resolved will be skipped and is not re-resolved. In general, callers must not assume any
+     * relationship between an artifact's resolved filename and its coordinates. Note that this method assumes that any
+     * relocations have already been processed.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The resolution request, must not be {@code null}.
+     * @return The resolution result, never {@code null}.
+     * @throws ArtifactResolutionException If the artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    ArtifactResult resolveArtifact( RepositorySystemSession session, ArtifactRequest request )
+        throws ArtifactResolutionException;
+
+    /**
+     * Resolves the paths for a collection of artifacts. Artifacts will be downloaded to the local repository if
+     * necessary. Artifacts that are already resolved will be skipped and are not re-resolved. In general, callers must
+     * not assume any relationship between an artifact's filename and its coordinates. Note that this method assumes
+     * that any relocations have already been processed.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @throws ArtifactResolutionException If any artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    List<ArtifactResult> resolveArtifacts( RepositorySystemSession session,
+                                           Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException;
+
+    /**
+     * Resolves the paths for a collection of metadata. Metadata will be downloaded to the local repository if
+     * necessary, e.g. because it hasn't been cached yet or the cache is deemed outdated.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @see Metadata#getFile()
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    List<MetadataResult> resolveMetadata( RepositorySystemSession session,
+                                          Collection<? extends MetadataRequest> requests );
+
+    /**
+     * Installs a collection of artifacts and their accompanying metadata to the local repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The installation request, must not be {@code null}.
+     * @return The installation result, never {@code null}.
+     * @throws InstallationException If any artifact/metadata from the request could not be installed.
+     */
+    InstallResult install( RepositorySystemSession session, InstallRequest request )
+        throws InstallationException;
+
+    /**
+     * Uploads a collection of artifacts and their accompanying metadata to a remote repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The deployment request, must not be {@code null}.
+     * @return The deployment result, never {@code null}.
+     * @throws DeploymentException If any artifact/metadata from the request could not be deployed.
+     * @see #newDeploymentRepository(RepositorySystemSession, RemoteRepository)
+     */
+    DeployResult deploy( RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException;
+
+    /**
+     * Creates a new manager for the specified local repository. If the specified local repository has no type, the
+     * default local repository type of the system will be used. <em>Note:</em> It is expected that this method
+     * invocation is one of the last steps of setting up a new session, in particular any configuration properties
+     * should have been set already.
+     * 
+     * @param session The repository system session from which to configure the manager, must not be {@code null}.
+     * @param localRepository The local repository to create a manager for, must not be {@code null}.
+     * @return The local repository manager, never {@code null}.
+     * @throws IllegalArgumentException If the specified repository type is not recognized or no base directory is
+     *             given.
+     */
+    LocalRepositoryManager newLocalRepositoryManager( RepositorySystemSession session, LocalRepository localRepository );
+
+    /**
+     * Creates a new synchronization context.
+     * 
+     * @param session The repository session during which the context will be used, must not be {@code null}.
+     * @param shared A flag indicating whether access to the artifacts/metadata associated with the new context can be
+     *            shared among concurrent readers or whether access needs to be exclusive to the calling thread.
+     * @return The synchronization context, never {@code null}.
+     */
+    SyncContext newSyncContext( RepositorySystemSession session, boolean shared );
+
+    /**
+     * Forms remote repositories suitable for artifact resolution by applying the session's authentication selector and
+     * similar network configuration to the given repository prototypes. As noted for
+     * {@link RepositorySystemSession#getAuthenticationSelector()} etc. the remote repositories passed to e.g.
+     * {@link #resolveArtifact(RepositorySystemSession, ArtifactRequest) resolveArtifact()} are used as is and expected
+     * to already carry any required authentication or proxy configuration. This method can be used to apply the
+     * authentication/proxy configuration from a session to a bare repository definition to obtain the complete
+     * repository definition for use in the resolution request.
+     * 
+     * @param session The repository system session from which to configure the repositories, must not be {@code null}.
+     * @param repositories The repository prototypes from which to derive the resolution repositories, must not be
+     *            {@code null} or contain {@code null} elements.
+     * @return The resolution repositories, never {@code null}. Note that there is generally no 1:1 relationship of the
+     *         obtained repositories to the original inputs due to mirror selection potentially aggregating multiple
+     *         repositories.
+     * @see #newDeploymentRepository(RepositorySystemSession, RemoteRepository)
+     */
+    List<RemoteRepository> newResolutionRepositories( RepositorySystemSession session,
+                                                      List<RemoteRepository> repositories );
+
+    /**
+     * Forms a remote repository suitable for artifact deployment by applying the session's authentication selector and
+     * similar network configuration to the given repository prototype. As noted for
+     * {@link RepositorySystemSession#getAuthenticationSelector()} etc. the remote repository passed to
+     * {@link #deploy(RepositorySystemSession, DeployRequest) deploy()} is used as is and expected to already carry any
+     * required authentication or proxy configuration. This method can be used to apply the authentication/proxy
+     * configuration from a session to a bare repository definition to obtain the complete repository definition for use
+     * in the deploy request.
+     * 
+     * @param session The repository system session from which to configure the repository, must not be {@code null}.
+     * @param repository The repository prototype from which to derive the deployment repository, must not be
+     *            {@code null}.
+     * @return The deployment repository, never {@code null}.
+     * @see #newResolutionRepositories(RepositorySystemSession, List)
+     */
+    RemoteRepository newDeploymentRepository( RepositorySystemSession session, RemoteRepository repository );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
new file mode 100644
index 0000000..888f29c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
@@ -0,0 +1,263 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * Defines settings and components that control the repository system. Once initialized, the session object itself is
+ * supposed to be immutable and hence can safely be shared across an entire application and any concurrent threads
+ * reading it. Components that wish to tweak some aspects of an existing session should use the copy constructor of
+ * {@link DefaultRepositorySystemSession} and its mutators to derive a custom session.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositorySystemSession
+{
+
+    /**
+     * Indicates whether the repository system operates in offline mode and avoids/refuses any access to remote
+     * repositories.
+     * 
+     * @return {@code true} if the repository system is in offline mode, {@code false} otherwise.
+     */
+    boolean isOffline();
+
+    /**
+     * Indicates whether repositories declared in artifact descriptors should be ignored during transitive dependency
+     * collection. If enabled, only the repositories originally provided with the collect request will be considered.
+     * 
+     * @return {@code true} if additional repositories from artifact descriptors are ignored, {@code false} to merge
+     *         those with the originally specified repositories.
+     */
+    boolean isIgnoreArtifactDescriptorRepositories();
+
+    /**
+     * Gets the policy which controls whether resolutions errors from remote repositories should be cached.
+     * 
+     * @return The resolution error policy for this session or {@code null} if resolution errors should generally not be
+     *         cached.
+     */
+    ResolutionErrorPolicy getResolutionErrorPolicy();
+
+    /**
+     * Gets the policy which controls how errors related to reading artifact descriptors should be handled.
+     * 
+     * @return The descriptor error policy for this session or {@code null} if descriptor errors should generally not be
+     *         tolerated.
+     */
+    ArtifactDescriptorPolicy getArtifactDescriptorPolicy();
+
+    /**
+     * Gets the global checksum policy. If set, the global checksum policy overrides the checksum policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @return The global checksum policy or {@code null}/empty if not set and the per-repository policies apply.
+     * @see RepositoryPolicy#CHECKSUM_POLICY_FAIL
+     * @see RepositoryPolicy#CHECKSUM_POLICY_IGNORE
+     * @see RepositoryPolicy#CHECKSUM_POLICY_WARN
+     */
+    String getChecksumPolicy();
+
+    /**
+     * Gets the global update policy. If set, the global update policy overrides the update policies of the remote
+     * repositories being used for resolution.
+     * 
+     * @return The global update policy or {@code null}/empty if not set and the per-repository policies apply.
+     * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS
+     * @see RepositoryPolicy#UPDATE_POLICY_DAILY
+     * @see RepositoryPolicy#UPDATE_POLICY_NEVER
+     */
+    String getUpdatePolicy();
+
+    /**
+     * Gets the local repository used during this session. This is a convenience method for
+     * {@link LocalRepositoryManager#getRepository()}.
+     * 
+     * @return The local repository being during this session, never {@code null}.
+     */
+    LocalRepository getLocalRepository();
+
+    /**
+     * Gets the local repository manager used during this session.
+     * 
+     * @return The local repository manager used during this session, never {@code null}.
+     */
+    LocalRepositoryManager getLocalRepositoryManager();
+
+    /**
+     * Gets the workspace reader used during this session. If set, the workspace reader will usually be consulted first
+     * to resolve artifacts.
+     * 
+     * @return The workspace reader for this session or {@code null} if none.
+     */
+    WorkspaceReader getWorkspaceReader();
+
+    /**
+     * Gets the listener being notified of actions in the repository system.
+     * 
+     * @return The repository listener or {@code null} if none.
+     */
+    RepositoryListener getRepositoryListener();
+
+    /**
+     * Gets the listener being notified of uploads/downloads by the repository system.
+     * 
+     * @return The transfer listener or {@code null} if none.
+     */
+    TransferListener getTransferListener();
+
+    /**
+     * Gets the system properties to use, e.g. for processing of artifact descriptors. System properties are usually
+     * collected from the runtime environment like {@link System#getProperties()} and environment variables.
+     * 
+     * @return The (read-only) system properties, never {@code null}.
+     */
+    Map<String, String> getSystemProperties();
+
+    /**
+     * Gets the user properties to use, e.g. for processing of artifact descriptors. User properties are similar to
+     * system properties but are set on the discretion of the user and hence are considered of higher priority than
+     * system properties.
+     * 
+     * @return The (read-only) user properties, never {@code null}.
+     */
+    Map<String, String> getUserProperties();
+
+    /**
+     * Gets the configuration properties used to tweak internal aspects of the repository system (e.g. thread pooling,
+     * connector-specific behavior, etc.)
+     * 
+     * @return The (read-only) configuration properties, never {@code null}.
+     * @see ConfigurationProperties
+     */
+    Map<String, Object> getConfigProperties();
+
+    /**
+     * Gets the mirror selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to denote the effective repositories.
+     * 
+     * @return The mirror selector to use, never {@code null}.
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    MirrorSelector getMirrorSelector();
+
+    /**
+     * Gets the proxy selector to use for repositories discovered in artifact descriptors. Note that this selector is
+     * not used for remote repositories which are passed as request parameters to the repository system, those
+     * repositories are supposed to have their proxy (if any) already set.
+     * 
+     * @return The proxy selector to use, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getProxy()
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    ProxySelector getProxySelector();
+
+    /**
+     * Gets the authentication selector to use for repositories discovered in artifact descriptors. Note that this
+     * selector is not used for remote repositories which are passed as request parameters to the repository system,
+     * those repositories are supposed to have their authentication (if any) already set.
+     * 
+     * @return The authentication selector to use, never {@code null}.
+     * @see org.eclipse.aether.repository.RemoteRepository#getAuthentication()
+     * @see RepositorySystem#newResolutionRepositories(RepositorySystemSession, java.util.List)
+     */
+    AuthenticationSelector getAuthenticationSelector();
+
+    /**
+     * Gets the registry of artifact types recognized by this session, for instance when processing artifact
+     * descriptors.
+     * 
+     * @return The artifact type registry, never {@code null}.
+     */
+    ArtifactTypeRegistry getArtifactTypeRegistry();
+
+    /**
+     * Gets the dependency traverser to use for building dependency graphs.
+     * 
+     * @return The dependency traverser to use for building dependency graphs or {@code null} if dependencies are
+     *         unconditionally traversed.
+     */
+    DependencyTraverser getDependencyTraverser();
+
+    /**
+     * Gets the dependency manager to use for building dependency graphs.
+     * 
+     * @return The dependency manager to use for building dependency graphs or {@code null} if dependency management is
+     *         not performed.
+     */
+    DependencyManager getDependencyManager();
+
+    /**
+     * Gets the dependency selector to use for building dependency graphs.
+     * 
+     * @return The dependency selector to use for building dependency graphs or {@code null} if dependencies are
+     *         unconditionally included.
+     */
+    DependencySelector getDependencySelector();
+
+    /**
+     * Gets the version filter to use for building dependency graphs.
+     * 
+     * @return The version filter to use for building dependency graphs or {@code null} if versions aren't filtered.
+     */
+    VersionFilter getVersionFilter();
+
+    /**
+     * Gets the dependency graph transformer to use for building dependency graphs.
+     * 
+     * @return The dependency graph transformer to use for building dependency graphs or {@code null} if none.
+     */
+    DependencyGraphTransformer getDependencyGraphTransformer();
+
+    /**
+     * Gets the custom data associated with this session.
+     * 
+     * @return The session data, never {@code null}.
+     */
+    SessionData getData();
+
+    /**
+     * Gets the cache the repository system may use to save data for future reuse during the session.
+     * 
+     * @return The repository cache or {@code null} if none.
+     */
+    RepositoryCache getCache();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RequestTrace.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RequestTrace.java
new file mode 100644
index 0000000..86aaa78
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RequestTrace.java
@@ -0,0 +1,117 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * A trace of nested requests that are performed by the repository system. This trace information can be used to
+ * correlate repository events with higher level operations in the application code that eventually caused the events. A
+ * single trace can carry an arbitrary object as data which is meant to describe a request/operation that is currently
+ * executed. For call hierarchies within the repository system itself, this data will usually be the {@code *Request}
+ * object that is currently processed. When invoking methods on the repository system, client code may provide a request
+ * trace that has been prepopulated with whatever data is useful for the application to indicate its state for later
+ * evaluation when processing the repository events.
+ * 
+ * @see RepositoryEvent#getTrace()
+ */
+public class RequestTrace
+{
+
+    private final RequestTrace parent;
+
+    private final Object data;
+
+    /**
+     * Creates a child of the specified request trace. This method is basically a convenience that will invoke
+     * {@link RequestTrace#newChild(Object) parent.newChild()} when the specified parent trace is not {@code null} or
+     * otherwise instantiante a new root trace.
+     * 
+     * @param parent The parent request trace, may be {@code null}.
+     * @param data The data to associate with the child trace, may be {@code null}.
+     * @return The child trace, never {@code null}.
+     */
+    public static RequestTrace newChild( RequestTrace parent, Object data )
+    {
+        if ( parent == null )
+        {
+            return new RequestTrace( data );
+        }
+        return parent.newChild( data );
+    }
+
+    /**
+     * Creates a new root trace with the specified data.
+     * 
+     * @param data The data to associate with the trace, may be {@code null}.
+     */
+    public RequestTrace( Object data )
+    {
+        this( null, data );
+    }
+
+    /**
+     * Creates a new trace with the specified data and parent
+     * 
+     * @param parent The parent trace, may be {@code null} for a root trace.
+     * @param data The data to associate with the trace, may be {@code null}.
+     */
+    protected RequestTrace( RequestTrace parent, Object data )
+    {
+        this.parent = parent;
+        this.data = data;
+    }
+
+    /**
+     * Gets the data associated with this trace.
+     * 
+     * @return The data associated with this trace or {@code null} if none.
+     */
+    public final Object getData()
+    {
+        return data;
+    }
+
+    /**
+     * Gets the parent of this trace.
+     * 
+     * @return The parent of this trace or {@code null} if this is the root of the trace stack.
+     */
+    public final RequestTrace getParent()
+    {
+        return parent;
+    }
+
+    /**
+     * Creates a new child of this trace.
+     * 
+     * @param data The data to associate with the child, may be {@code null}.
+     * @return The child trace, never {@code null}.
+     */
+    public RequestTrace newChild( Object data )
+    {
+        return new RequestTrace( this, data );
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getData() );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/SessionData.java b/maven-resolver-api/src/main/java/org/eclipse/aether/SessionData.java
new file mode 100644
index 0000000..b6efeac
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/SessionData.java
@@ -0,0 +1,66 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+/**
+ * A container for data that is specific to a repository system session. Both components within the repository system
+ * and clients of the system may use this storage to associate arbitrary data with a session.
+ * <p>
+ * Unlike a cache, this session data is not subject to purging. For this same reason, session data should also not be
+ * abused as a cache (i.e. for storing values that can be re-calculated) to avoid memory exhaustion.
+ * <p>
+ * <strong>Note:</strong> Actual implementations must be thread-safe.
+ * 
+ * @see RepositorySystemSession#getData()
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface SessionData
+{
+
+    /**
+     * Associates the specified session data with the given key.
+     * 
+     * @param key The key under which to store the session data, must not be {@code null}.
+     * @param value The data to associate with the key, may be {@code null} to remove the mapping.
+     */
+    void set( Object key, Object value );
+
+    /**
+     * Associates the specified session data with the given key if the key is currently mapped to the given value. This
+     * method provides an atomic compare-and-update of some key's value.
+     * 
+     * @param key The key under which to store the session data, must not be {@code null}.
+     * @param oldValue The expected data currently associated with the key, may be {@code null}.
+     * @param newValue The data to associate with the key, may be {@code null} to remove the mapping.
+     * @return {@code true} if the key mapping was successfully updated from the old value to the new value,
+     *         {@code false} if the current key mapping didn't match the expected value and was not updated.
+     */
+    boolean set( Object key, Object oldValue, Object newValue );
+
+    /**
+     * Gets the session data associated with the specified key.
+     * 
+     * @param key The key for which to retrieve the session data, must not be {@code null}.
+     * @return The session data associated with the key or {@code null} if none.
+     */
+    Object get( Object key );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/SyncContext.java b/maven-resolver-api/src/main/java/org/eclipse/aether/SyncContext.java
new file mode 100644
index 0000000..2d751c0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/SyncContext.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A synchronization context used to coordinate concurrent access to artifacts or metadatas. The typical usage of a
+ * synchronization context looks like this:
+ * 
+ * <pre>
+ * SyncContext syncContext = repositorySystem.newSyncContext( ... );
+ * try {
+ *     syncContext.acquire( artifacts, metadatas );
+ *     // work with the artifacts and metadatas
+ * } finally {
+ *     syncContext.close();
+ * }
+ * </pre>
+ * 
+ * Within one thread, synchronization contexts may be nested which can naturally happen in a hierarchy of method calls.
+ * The nested synchronization contexts may also acquire overlapping sets of artifacts/metadatas as long as the following
+ * conditions are met. If the outer-most context holding a particular resource is exclusive, that resource can be
+ * reacquired in any nested context. If however the outer-most context is shared, the resource may only be reacquired by
+ * nested contexts if these are also shared.
+ * <p>
+ * A synchronization context is meant to be utilized by only one thread and as such is not thread-safe.
+ * <p>
+ * Note that the level of actual synchronization is subject to the implementation and might range from OS-wide to none.
+ * 
+ * @see RepositorySystem#newSyncContext(RepositorySystemSession, boolean)
+ */
+public interface SyncContext
+    extends Closeable
+{
+
+    /**
+     * Acquires synchronized access to the specified artifacts and metadatas. The invocation will potentially block
+     * until all requested resources can be acquired by the calling thread. Acquiring resources that are already
+     * acquired by this synchronization context has no effect. Please also see the class-level documentation for
+     * information regarding reentrancy. The method may be invoked multiple times on a synchronization context until all
+     * desired resources have been acquired.
+     * 
+     * @param artifacts The artifacts to acquire, may be {@code null} or empty if none.
+     * @param metadatas The metadatas to acquire, may be {@code null} or empty if none.
+     */
+    void acquire( Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas );
+
+    /**
+     * Releases all previously acquired artifacts/metadatas. If no resources have been acquired before or if this
+     * synchronization context has already been closed, this method does nothing.
+     */
+    void close();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java
new file mode 100644
index 0000000..d89260b
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java
@@ -0,0 +1,230 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A skeleton class for artifacts.
+ */
+public abstract class AbstractArtifact
+    implements Artifact
+{
+
+    private static final String SNAPSHOT = "SNAPSHOT";
+
+    private static final Pattern SNAPSHOT_TIMESTAMP = Pattern.compile( "^(.*-)?([0-9]{8}\\.[0-9]{6}-[0-9]+)$" );
+
+    public boolean isSnapshot()
+    {
+        return isSnapshot( getVersion() );
+    }
+
+    private static boolean isSnapshot( String version )
+    {
+        return version.endsWith( SNAPSHOT ) || SNAPSHOT_TIMESTAMP.matcher( version ).matches();
+    }
+
+    public String getBaseVersion()
+    {
+        return toBaseVersion( getVersion() );
+    }
+
+    private static String toBaseVersion( String version )
+    {
+        String baseVersion;
+
+        if ( version == null )
+        {
+            baseVersion = version;
+        }
+        else if ( version.startsWith( "[" ) || version.startsWith( "(" ) )
+        {
+            baseVersion = version;
+        }
+        else
+        {
+            Matcher m = SNAPSHOT_TIMESTAMP.matcher( version );
+            if ( m.matches() )
+            {
+                if ( m.group( 1 ) != null )
+                {
+                    baseVersion = m.group( 1 ) + SNAPSHOT;
+                }
+                else
+                {
+                    baseVersion = SNAPSHOT;
+                }
+            }
+            else
+            {
+                baseVersion = version;
+            }
+        }
+
+        return baseVersion;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates, properties and file.
+     * 
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none. The method may assume immutability
+     *            of the supplied map, i.e. need not copy it.
+     * @param file The resolved file of the artifact, may be {@code null}.
+     * @return The new artifact instance, never {@code null}.
+     */
+    private Artifact newInstance( String version, Map<String, String> properties, File file )
+    {
+        return new DefaultArtifact( getGroupId(), getArtifactId(), getClassifier(), getExtension(), version, file,
+                                    properties );
+    }
+
+    public Artifact setVersion( String version )
+    {
+        String current = getVersion();
+        if ( current.equals( version ) || ( version == null && current.length() <= 0 ) )
+        {
+            return this;
+        }
+        return newInstance( version, getProperties(), getFile() );
+    }
+
+    public Artifact setFile( File file )
+    {
+        File current = getFile();
+        if ( ( current == null ) ? file == null : current.equals( file ) )
+        {
+            return this;
+        }
+        return newInstance( getVersion(), getProperties(), file );
+    }
+
+    public Artifact setProperties( Map<String, String> properties )
+    {
+        Map<String, String> current = getProperties();
+        if ( current.equals( properties ) || ( properties == null && current.isEmpty() ) )
+        {
+            return this;
+        }
+        return newInstance( getVersion(), copyProperties( properties ), getFile() );
+    }
+
+    public String getProperty( String key, String defaultValue )
+    {
+        String value = getProperties().get( key );
+        return ( value != null ) ? value : defaultValue;
+    }
+
+    /**
+     * Copies the specified artifact properties. This utility method should be used when creating new artifact instances
+     * with caller-supplied properties.
+     * 
+     * @param properties The properties to copy, may be {@code null}.
+     * @return The copied and read-only properties, never {@code null}.
+     */
+    protected static Map<String, String> copyProperties( Map<String, String> properties )
+    {
+        if ( properties != null && !properties.isEmpty() )
+        {
+            return Collections.unmodifiableMap( new HashMap<String, String>( properties ) );
+        }
+        else
+        {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+        buffer.append( getGroupId() );
+        buffer.append( ':' ).append( getArtifactId() );
+        buffer.append( ':' ).append( getExtension() );
+        if ( getClassifier().length() > 0 )
+        {
+            buffer.append( ':' ).append( getClassifier() );
+        }
+        buffer.append( ':' ).append( getVersion() );
+        return buffer.toString();
+    }
+
+    /**
+     * Compares this artifact with the specified object.
+     * 
+     * @param obj The object to compare this artifact against, may be {@code null}.
+     * @return {@code true} if and only if the specified object is another {@link Artifact} with equal coordinates,
+     *         properties and file, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( !( obj instanceof Artifact ) )
+        {
+            return false;
+        }
+
+        Artifact that = (Artifact) obj;
+
+        return getArtifactId().equals( that.getArtifactId() ) && getGroupId().equals( that.getGroupId() )
+            && getVersion().equals( that.getVersion() ) && getExtension().equals( that.getExtension() )
+            && getClassifier().equals( that.getClassifier() ) && eq( getFile(), that.getFile() )
+            && getProperties().equals( that.getProperties() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    /**
+     * Returns a hash code for this artifact.
+     * 
+     * @return A hash code for the artifact.
+     */
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getGroupId().hashCode();
+        hash = hash * 31 + getArtifactId().hashCode();
+        hash = hash * 31 + getExtension().hashCode();
+        hash = hash * 31 + getClassifier().hashCode();
+        hash = hash * 31 + getVersion().hashCode();
+        hash = hash * 31 + hash( getFile() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return ( obj != null ) ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java
new file mode 100644
index 0000000..6323243
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java
@@ -0,0 +1,143 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A specific artifact. In a nutshell, an artifact has identifying coordinates and optionally a file that denotes its
+ * data. <em>Note:</em> Artifact instances are supposed to be immutable, e.g. any exposed mutator method returns a new
+ * artifact instance and leaves the original instance unchanged. <em>Note:</em> Implementors are strongly advised to
+ * inherit from {@link AbstractArtifact} instead of directly implementing this interface.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface Artifact
+{
+
+    /**
+     * Gets the group identifier of this artifact, for example "org.apache.maven".
+     * 
+     * @return The group identifier, never {@code null}.
+     */
+    String getGroupId();
+
+    /**
+     * Gets the artifact identifier of this artifact, for example "maven-model".
+     * 
+     * @return The artifact identifier, never {@code null}.
+     */
+    String getArtifactId();
+
+    /**
+     * Gets the version of this artifact, for example "1.0-20100529-1213". Note that in case of meta versions like
+     * "1.0-SNAPSHOT", the artifact's version depends on the state of the artifact. Artifacts that have been resolved or
+     * deployed will usually have the meta version expanded.
+     * 
+     * @return The version, never {@code null}.
+     */
+    String getVersion();
+
+    /**
+     * Sets the version of the artifact.
+     * 
+     * @param version The version of this artifact, may be {@code null} or empty.
+     * @return The new artifact, never {@code null}.
+     */
+    Artifact setVersion( String version );
+
+    /**
+     * Gets the base version of this artifact, for example "1.0-SNAPSHOT". In contrast to the {@link #getVersion()}, the
+     * base version will always refer to the unresolved meta version.
+     * 
+     * @return The base version, never {@code null}.
+     */
+    String getBaseVersion();
+
+    /**
+     * Determines whether this artifact uses a snapshot version.
+     * 
+     * @return {@code true} if the artifact is a snapshot, {@code false} otherwise.
+     */
+    boolean isSnapshot();
+
+    /**
+     * Gets the classifier of this artifact, for example "sources".
+     * 
+     * @return The classifier or an empty string if none, never {@code null}.
+     */
+    String getClassifier();
+
+    /**
+     * Gets the (file) extension of this artifact, for example "jar" or "tar.gz".
+     * 
+     * @return The file extension (without leading period), never {@code null}.
+     */
+    String getExtension();
+
+    /**
+     * Gets the file of this artifact. Note that only resolved artifacts have a file associated with them. In general,
+     * callers must not assume any relationship between an artifact's filename and its coordinates.
+     * 
+     * @return The file or {@code null} if the artifact isn't resolved.
+     */
+    File getFile();
+
+    /**
+     * Sets the file of the artifact.
+     * 
+     * @param file The file of the artifact, may be {@code null}
+     * @return The new artifact, never {@code null}.
+     */
+    Artifact setFile( File file );
+
+    /**
+     * Gets the specified property.
+     * 
+     * @param key The name of the property, must not be {@code null}.
+     * @param defaultValue The default value to return in case the property is not set, may be {@code null}.
+     * @return The requested property value or {@code null} if the property is not set and no default value was
+     *         provided.
+     * @see ArtifactProperties
+     */
+    String getProperty( String key, String defaultValue );
+
+    /**
+     * Gets the properties of this artifact. Clients may use these properties to associate non-persistent values with an
+     * artifact that help later processing when the artifact gets passed around within the application.
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * Sets the properties for the artifact. Note that these properties exist merely in memory and are not persisted
+     * when the artifact gets installed/deployed to a repository.
+     * 
+     * @param properties The properties for the artifact, may be {@code null}.
+     * @return The new artifact, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Artifact setProperties( Map<String, String> properties );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactProperties.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactProperties.java
new file mode 100644
index 0000000..1108086
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactProperties.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+/**
+ * The keys for common properties of artifacts.
+ * 
+ * @see Artifact#getProperties()
+ */
+public final class ArtifactProperties
+{
+
+    /**
+     * A high-level characterization of the artifact, e.g. "maven-plugin" or "test-jar".
+     * 
+     * @see ArtifactType#getId()
+     */
+    public static final String TYPE = "type";
+
+    /**
+     * The programming language this artifact is relevant for, e.g. "java" or "none".
+     */
+    public static final String LANGUAGE = "language";
+
+    /**
+     * The (expected) path to the artifact on the local filesystem. An artifact which has this property set is assumed
+     * to be not present in any regular repository and likewise has no artifact descriptor. Artifact resolution will
+     * verify the path and resolve the artifact if the path actually denotes an existing file. If the path isn't valid,
+     * resolution will fail and no attempts to search local/remote repositories are made.
+     */
+    public static final String LOCAL_PATH = "localPath";
+
+    /**
+     * A boolean flag indicating whether the artifact presents some kind of bundle that physically includes its
+     * dependencies, e.g. a fat WAR.
+     */
+    public static final String INCLUDES_DEPENDENCIES = "includesDependencies";
+
+    /**
+     * A boolean flag indicating whether the artifact is meant to be used for the compile/runtime/test build path of a
+     * consumer project.
+     */
+    public static final String CONSTITUTES_BUILD_PATH = "constitutesBuildPath";
+
+    /**
+     * The URL to a web page from which the artifact can be manually downloaded. This URL is not contacted by the
+     * repository system but serves as a pointer for the end user to assist in getting artifacts that are not published
+     * in a proper repository.
+     */
+    public static final String DOWNLOAD_URL = "downloadUrl";
+
+    private ArtifactProperties()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactType.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactType.java
new file mode 100644
index 0000000..5f87217
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactType.java
@@ -0,0 +1,67 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+
+/**
+ * An artifact type describing artifact characteristics/properties that are common for certain artifacts. Artifact types
+ * are a means to simplify the description of an artifact by referring to an artifact type instead of specifying the
+ * various properties individually.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @see ArtifactTypeRegistry
+ * @see DefaultArtifact#DefaultArtifact(String, String, String, String, String, ArtifactType)
+ */
+public interface ArtifactType
+{
+
+    /**
+     * Gets the identifier of this type, e.g. "maven-plugin" or "test-jar".
+     * 
+     * @return The identifier of this type, never {@code null}.
+     * @see ArtifactProperties#TYPE
+     */
+    String getId();
+
+    /**
+     * Gets the file extension to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The usual file extension, never {@code null}.
+     */
+    String getExtension();
+
+    /**
+     * Gets the classifier to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The usual classifier or an empty string if none, never {@code null}.
+     */
+    String getClassifier();
+
+    /**
+     * Gets the properties to use for artifacts of this type (unless explicitly overridden by the artifact).
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     * @see ArtifactProperties
+     */
+    Map<String, String> getProperties();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactTypeRegistry.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactTypeRegistry.java
new file mode 100644
index 0000000..f379173
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/ArtifactTypeRegistry.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+/**
+ * A registry of known artifact types.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getArtifactTypeRegistry()
+ */
+public interface ArtifactTypeRegistry
+{
+
+    /**
+     * Gets the artifact type with the specified identifier.
+     * 
+     * @param typeId The identifier of the type, must not be {@code null}.
+     * @return The artifact type or {@code null} if no type with the requested identifier exists.
+     */
+    ArtifactType get( String typeId );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java
new file mode 100644
index 0000000..786af74
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java
@@ -0,0 +1,285 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A simple artifact. <em>Note:</em> Instances of this class are immutable and the exposed mutators return new objects
+ * rather than changing the current instance.
+ */
+public final class DefaultArtifact
+    extends AbstractArtifact
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String version;
+
+    private final String classifier;
+
+    private final String extension;
+
+    private final File file;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new artifact with the specified coordinates. If not specified in the artifact coordinates, the
+     * artifact's extension defaults to {@code jar} and classifier to an empty string.
+     * 
+     * @param coords The artifact coordinates in the format
+     *            {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}, must not be {@code null}.
+     */
+    public DefaultArtifact( String coords )
+    {
+        this( coords, Collections.<String, String>emptyMap() );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and properties. If not specified in the artifact
+     * coordinates, the artifact's extension defaults to {@code jar} and classifier to an empty string.
+     * 
+     * @param coords The artifact coordinates in the format
+     *            {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>}, must not be {@code null}.
+     * @param properties The artifact properties, may be {@code null}.
+     */
+    public DefaultArtifact( String coords, Map<String, String> properties )
+    {
+        Pattern p = Pattern.compile( "([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)" );
+        Matcher m = p.matcher( coords );
+        if ( !m.matches() )
+        {
+            throw new IllegalArgumentException( "Bad artifact coordinates " + coords
+                + ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>" );
+        }
+        groupId = m.group( 1 );
+        artifactId = m.group( 2 );
+        extension = get( m.group( 4 ), "jar" );
+        classifier = get( m.group( 6 ), "" );
+        version = m.group( 7 );
+        file = null;
+        this.properties = copyProperties( properties );
+    }
+
+    private static String get( String value, String defaultValue )
+    {
+        return ( value == null || value.length() <= 0 ) ? defaultValue : value;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and no classifier. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String extension, String version )
+    {
+        this( groupId, artifactId, "", extension, version );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates. Passing {@code null} for any of the coordinates is
+     * equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version )
+    {
+        this( groupId, artifactId, classifier, extension, version, null, (File) null );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates. Passing {@code null} for any of the coordinates is
+     * equivalent to specifying an empty string. The optional artifact type provided to this constructor will be used to
+     * determine the artifact's classifier and file extension if the corresponding arguments for this constructor are
+     * {@code null}.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param type The artifact type from which to query classifier, file extension and properties, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            ArtifactType type )
+    {
+        this( groupId, artifactId, classifier, extension, version, null, type );
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates and properties. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string. The optional artifact type provided to this constructor
+     * will be used to determine the artifact's classifier and file extension if the corresponding arguments for this
+     * constructor are {@code null}. If the artifact type specifies properties, those will get merged with the
+     * properties passed directly into the constructor, with the latter properties taking precedence.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none.
+     * @param type The artifact type from which to query classifier, file extension and properties, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            Map<String, String> properties, ArtifactType type )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        if ( classifier != null || type == null )
+        {
+            this.classifier = emptify( classifier );
+        }
+        else
+        {
+            this.classifier = emptify( type.getClassifier() );
+        }
+        if ( extension != null || type == null )
+        {
+            this.extension = emptify( extension );
+        }
+        else
+        {
+            this.extension = emptify( type.getExtension() );
+        }
+        this.version = emptify( version );
+        this.file = null;
+        this.properties = merge( properties, ( type != null ) ? type.getProperties() : null );
+    }
+
+    private static Map<String, String> merge( Map<String, String> dominant, Map<String, String> recessive )
+    {
+        Map<String, String> properties;
+
+        if ( ( dominant == null || dominant.isEmpty() ) && ( recessive == null || recessive.isEmpty() ) )
+        {
+            properties = Collections.emptyMap();
+        }
+        else
+        {
+            properties = new HashMap<String, String>();
+            if ( recessive != null )
+            {
+                properties.putAll( recessive );
+            }
+            if ( dominant != null )
+            {
+                properties.putAll( dominant );
+            }
+            properties = Collections.unmodifiableMap( properties );
+        }
+
+        return properties;
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates, properties and file. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string.
+     * 
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none.
+     * @param file The resolved file of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version,
+                            Map<String, String> properties, File file )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.classifier = emptify( classifier );
+        this.extension = emptify( extension );
+        this.version = emptify( version );
+        this.file = file;
+        this.properties = copyProperties( properties );
+    }
+
+    DefaultArtifact( String groupId, String artifactId, String classifier, String extension, String version, File file,
+                     Map<String, String> properties )
+    {
+        // NOTE: This constructor assumes immutability of the provided properties, for internal use only
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.classifier = emptify( classifier );
+        this.extension = emptify( extension );
+        this.version = emptify( version );
+        this.file = file;
+        this.properties = properties;
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    public File getFile()
+    {
+        return file;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifactType.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifactType.java
new file mode 100644
index 0000000..5ae6daa
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifactType.java
@@ -0,0 +1,147 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A simple artifact type.
+ */
+public final class DefaultArtifactType
+    implements ArtifactType
+{
+
+    private final String id;
+
+    private final String extension;
+
+    private final String classifier;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new artifact type with the specified identifier. This constructor assumes the usual file extension
+     * equals the given type id and that the usual classifier is empty. Additionally, the properties
+     * {@link ArtifactProperties#LANGUAGE}, {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} and
+     * {@link ArtifactProperties#INCLUDES_DEPENDENCIES} will be set to {@code "none"}, {@code true} and {@code false},
+     * respectively.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     */
+    public DefaultArtifactType( String id )
+    {
+        this( id, id, "", "none", false, false );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties. Additionally, the properties
+     * {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} and {@link ArtifactProperties#INCLUDES_DEPENDENCIES} will be
+     * set to {@code true} and {@code false}, respectively.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param language The value for the {@link ArtifactProperties#LANGUAGE} property, may be {@code null}.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, String language )
+    {
+        this( id, extension, classifier, language, true, false );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties.
+     * 
+     * @param id The identifier of the type which will also be used as the value for the {@link ArtifactProperties#TYPE}
+     *            property, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param language The value for the {@link ArtifactProperties#LANGUAGE} property, may be {@code null}.
+     * @param constitutesBuildPath The value for the {@link ArtifactProperties#CONSTITUTES_BUILD_PATH} property.
+     * @param includesDependencies The value for the {@link ArtifactProperties#INCLUDES_DEPENDENCIES} property.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, String language,
+                                boolean constitutesBuildPath, boolean includesDependencies )
+    {
+        this.id = requireNonNull( id, "type id cannot be null" );
+        if ( id.length() == 0 )
+        {
+            throw new IllegalArgumentException( "type id cannot be empty" );
+        }
+        this.extension = emptify( extension );
+        this.classifier = emptify( classifier );
+        Map<String, String> props = new HashMap<String, String>();
+        props.put( ArtifactProperties.TYPE, id );
+        props.put( ArtifactProperties.LANGUAGE, ( language != null && language.length() > 0 ) ? language : "none" );
+        props.put( ArtifactProperties.INCLUDES_DEPENDENCIES, Boolean.toString( includesDependencies ) );
+        props.put( ArtifactProperties.CONSTITUTES_BUILD_PATH, Boolean.toString( constitutesBuildPath ) );
+        properties = Collections.unmodifiableMap( props );
+    }
+
+    /**
+     * Creates a new artifact type with the specified properties.
+     * 
+     * @param id The identifier of the type, must not be {@code null} or empty.
+     * @param extension The usual file extension for artifacts of this type, may be {@code null}.
+     * @param classifier The usual classifier for artifacts of this type, may be {@code null}.
+     * @param properties The properties for artifacts of this type, may be {@code null}.
+     */
+    public DefaultArtifactType( String id, String extension, String classifier, Map<String, String> properties )
+    {
+        this.id = requireNonNull( id, "type id cannot be null" );
+        if ( id.length() == 0 )
+        {
+            throw new IllegalArgumentException( "type id cannot be empty" );
+        }
+        this.extension = emptify( extension );
+        this.classifier = emptify( classifier );
+        this.properties = AbstractArtifact.copyProperties( properties );
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getId()
+    {
+        return id;
+    }
+
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/package-info.java
new file mode 100644
index 0000000..9a4cc79
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The definition of an artifact, that is the primary entity managed by the repository system.
+ */
+package org.eclipse.aether.artifact;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectRequest.java
new file mode 100644
index 0000000..d9c2527
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectRequest.java
@@ -0,0 +1,356 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to collect the transitive dependencies and to build a dependency graph from them. There are three ways to
+ * create a dependency graph. First, only the root dependency can be given. Second, a root dependency and direct
+ * dependencies can be specified in which case the specified direct dependencies are merged with the direct dependencies
+ * retrieved from the artifact descriptor of the root dependency. And last, only direct dependencies can be specified in
+ * which case the root node of the resulting graph has no associated dependency.
+ * 
+ * @see RepositorySystem#collectDependencies(RepositorySystemSession, CollectRequest)
+ */
+public final class CollectRequest
+{
+
+    private Artifact rootArtifact;
+
+    private Dependency root;
+
+    private List<Dependency> dependencies = Collections.emptyList();
+
+    private List<Dependency> managedDependencies = Collections.emptyList();
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public CollectRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param root The root dependency whose transitive dependencies should be collected, may be {@code null}.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( Dependency root, List<RemoteRepository> repositories )
+    {
+        setRoot( root );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Creates a new request with the specified properties.
+     * 
+     * @param root The root dependency whose transitive dependencies should be collected, may be {@code null}.
+     * @param dependencies The direct dependencies to merge with the direct dependencies from the root dependency's
+     *            artifact descriptor.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( Dependency root, List<Dependency> dependencies, List<RemoteRepository> repositories )
+    {
+        setRoot( root );
+        setDependencies( dependencies );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Creates a new request with the specified properties.
+     * 
+     * @param dependencies The direct dependencies of some imaginary root, may be {@code null}.
+     * @param managedDependencies The dependency management information to apply to the transitive dependencies, may be
+     *            {@code null}.
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     */
+    public CollectRequest( List<Dependency> dependencies, List<Dependency> managedDependencies,
+                           List<RemoteRepository> repositories )
+    {
+        setDependencies( dependencies );
+        setManagedDependencies( managedDependencies );
+        setRepositories( repositories );
+    }
+
+    /**
+     * Gets the root artifact for the dependency graph.
+     * 
+     * @return The root artifact for the dependency graph or {@code null} if none.
+     */
+    public Artifact getRootArtifact()
+    {
+        return rootArtifact;
+    }
+
+    /**
+     * Sets the root artifact for the dependency graph. This must not be confused with {@link #setRoot(Dependency)}: The
+     * root <em>dependency</em>, like any other specified dependency, will be subject to dependency
+     * collection/resolution, i.e. should have an artifact descriptor and a corresponding artifact file. The root
+     * <em>artifact</em> on the other hand is only used as a label for the root node of the graph in case no root
+     * dependency was specified. As such, the configured root artifact is ignored if {@link #getRoot()} does not return
+     * {@code null}.
+     * 
+     * @param rootArtifact The root artifact for the dependency graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRootArtifact( Artifact rootArtifact )
+    {
+        this.rootArtifact = rootArtifact;
+        return this;
+    }
+
+    /**
+     * Gets the root dependency of the graph.
+     * 
+     * @return The root dependency of the graph or {@code null} if none.
+     */
+    public Dependency getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root dependency of the graph.
+     * 
+     * @param root The root dependency of the graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRoot( Dependency root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the direct dependencies.
+     * 
+     * @return The direct dependencies, never {@code null}.
+     */
+    public List<Dependency> getDependencies()
+    {
+        return dependencies;
+    }
+
+    /**
+     * Sets the direct dependencies. If both a root dependency and direct dependencies are given in the request, the
+     * direct dependencies from the request will be merged with the direct dependencies from the root dependency's
+     * artifact descriptor, giving higher priority to the dependencies from the request.
+     * 
+     * @param dependencies The direct dependencies, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.dependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.dependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified direct dependency.
+     * 
+     * @param dependency The dependency to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( this.dependencies.isEmpty() )
+            {
+                this.dependencies = new ArrayList<Dependency>();
+            }
+            this.dependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency management to apply to transitive dependencies.
+     * 
+     * @return The dependency management to apply to transitive dependencies, never {@code null}.
+     */
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    /**
+     * Sets the dependency management to apply to transitive dependencies. To clarify, this management does not apply to
+     * the direct dependencies of the root node.
+     * 
+     * @param managedDependencies The dependency management, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setManagedDependencies( List<Dependency> managedDependencies )
+    {
+        if ( managedDependencies == null )
+        {
+            this.managedDependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.managedDependencies = managedDependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified managed dependency.
+     * 
+     * @param managedDependency The managed dependency to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addManagedDependency( Dependency managedDependency )
+    {
+        if ( managedDependency != null )
+        {
+            if ( this.managedDependencies.isEmpty() )
+            {
+                this.managedDependencies = new ArrayList<Dependency>();
+            }
+            this.managedDependencies.add( managedDependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repositories to use for the collection.
+     * 
+     * @return The repositories to use for the collection, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to use for the collection.
+     * 
+     * @param repositories The repositories to use for the collection, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for collection.
+     * 
+     * @param repository The repository to collect dependency information from, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public CollectRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRoot() + " -> " + getDependencies() + " < " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectResult.java
new file mode 100644
index 0000000..53ebae4
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/CollectResult.java
@@ -0,0 +1,156 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * The result of a dependency collection request.
+ * 
+ * @see RepositorySystem#collectDependencies(RepositorySystemSession, CollectRequest)
+ */
+public final class CollectResult
+{
+
+    private final CollectRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<DependencyCycle> cycles;
+
+    private DependencyNode root;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public CollectResult( CollectRequest request )
+    {
+        this.request = requireNonNull( request, "dependency collection request cannot be null" );
+        exceptions = Collections.emptyList();
+        cycles = Collections.emptyList();
+    }
+
+    /**
+     * Gets the collection request that was made.
+     *
+     * @return The collection request, never {@code null}.
+     */
+    public CollectRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while building the dependency graph.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while building the dependency graph.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency cycles that were encountered while building the dependency graph.
+     * 
+     * @return The dependency cycles in the (raw) graph, never {@code null}.
+     */
+    public List<DependencyCycle> getCycles()
+    {
+        return cycles;
+    }
+
+    /**
+     * Records the specified dependency cycle.
+     * 
+     * @param cycle The dependency cycle to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult addCycle( DependencyCycle cycle )
+    {
+        if ( cycle != null )
+        {
+            if ( cycles.isEmpty() )
+            {
+                cycles = new ArrayList<DependencyCycle>();
+            }
+            cycles.add( cycle );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the root node of the dependency graph.
+     * 
+     * @return The root node of the dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the dependency graph.
+     * 
+     * @param root The root node of the dependency graph, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public CollectResult setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getRoot() );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionContext.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionContext.java
new file mode 100644
index 0000000..671bd2a
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionContext.java
@@ -0,0 +1,75 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A context used during dependency collection to update the dependency manager, selector and traverser.
+ * 
+ * @see DependencyManager#deriveChildManager(DependencyCollectionContext)
+ * @see DependencyTraverser#deriveChildTraverser(DependencyCollectionContext)
+ * @see DependencySelector#deriveChildSelector(DependencyCollectionContext)
+ * @see VersionFilter#deriveChildFilter(DependencyCollectionContext)
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyCollectionContext
+{
+
+    /**
+     * Gets the repository system session during which the dependency collection happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    RepositorySystemSession getSession();
+
+    /**
+     * Gets the artifact whose children are to be processed next during dependency collection. For all nodes but the
+     * root, this is simply shorthand for {@code getDependency().getArtifact()}. In case of the root node however,
+     * {@link #getDependency()} might be {@code null} while the node still has an artifact which serves as its label and
+     * is not to be resolved.
+     * 
+     * @return The artifact whose children are going to be processed or {@code null} in case of the root node without
+     *         dependency and label.
+     */
+    Artifact getArtifact();
+
+    /**
+     * Gets the dependency whose children are to be processed next during dependency collection.
+     * 
+     * @return The dependency whose children are going to be processed or {@code null} in case of the root node without
+     *         dependency.
+     */
+    Dependency getDependency();
+
+    /**
+     * Gets the dependency management information that was contributed by the artifact descriptor of the current
+     * dependency.
+     * 
+     * @return The dependency management information, never {@code null}.
+     */
+    List<Dependency> getManagedDependencies();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionException.java
new file mode 100644
index 0000000..8a04d79
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyCollectionException.java
@@ -0,0 +1,111 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of bad artifact descriptors, version ranges or other issues encountered during calculation of the
+ * dependency graph.
+ */
+public class DependencyCollectionException
+    extends RepositoryException
+{
+
+    private final transient CollectResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result )
+    {
+        super( "Failed to collect dependencies for " + getSource( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The collection result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyCollectionException( CollectResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the collection result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The collection result or {@code null} if unknown.
+     */
+    public CollectResult getResult()
+    {
+        return result;
+    }
+
+    private static String getSource( CollectResult result )
+    {
+        if ( result == null )
+        {
+            return "";
+        }
+
+        CollectRequest request = result.getRequest();
+        if ( request.getRoot() != null )
+        {
+            return request.getRoot().toString();
+        }
+        if ( request.getRootArtifact() != null )
+        {
+            return request.getRootArtifact().toString();
+        }
+
+        return request.getDependencies().toString();
+    }
+
+    private static Throwable getCause( CollectResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformationContext.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformationContext.java
new file mode 100644
index 0000000..ba66474
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformationContext.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A context used during dependency collection to exchange information within a chain of dependency graph transformers.
+ * 
+ * @see DependencyGraphTransformer
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyGraphTransformationContext
+{
+
+    /**
+     * Gets the repository system session during which the graph transformation happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    RepositorySystemSession getSession();
+
+    /**
+     * Gets a keyed value from the context.
+     * 
+     * @param key The key used to query the value, must not be {@code null}.
+     * @return The queried value or {@code null} if none.
+     */
+    Object get( Object key );
+
+    /**
+     * Puts a keyed value into the context.
+     * 
+     * @param key The key used to store the value, must not be {@code null}.
+     * @param value The value to store, may be {@code null} to remove the mapping.
+     * @return The previous value associated with the key or {@code null} if none.
+     */
+    Object put( Object key, Object value );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformer.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformer.java
new file mode 100644
index 0000000..c472500
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyGraphTransformer.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Transforms a given dependency graph.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> Dependency graphs may generally contain cycles. As such a graph transformer that cannot assume for
+ * sure that cycles have already been eliminated must gracefully handle cyclic graphs, e.g. guard against infinite
+ * recursion.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyGraphTransformer()
+ */
+public interface DependencyGraphTransformer
+{
+
+    /**
+     * Transforms the dependency graph denoted by the specified root node. The transformer may directly change the
+     * provided input graph or create a new graph, the former is recommended for performance reasons.
+     * 
+     * @param node The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+     * @param context The graph transformation context, must not be {@code null}.
+     * @return The result graph of the transformation, never {@code null}.
+     * @throws RepositoryException If the transformation failed.
+     */
+    DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException;
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java
new file mode 100644
index 0000000..054bfe0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java
@@ -0,0 +1,177 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+
+/**
+ * The management updates to apply to a dependency.
+ * 
+ * @see DependencyManager#manageDependency(Dependency)
+ */
+public final class DependencyManagement
+{
+
+    private String version;
+
+    private String scope;
+
+    private Boolean optional;
+
+    private Collection<Exclusion> exclusions;
+
+    private Map<String, String> properties;
+
+    /**
+     * Creates an empty management update.
+     */
+    public DependencyManagement()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Gets the new version to apply to the dependency.
+     * 
+     * @return The new version or {@code null} if the version is not managed and the existing dependency version should
+     *         remain unchanged.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the new version to apply to the dependency.
+     * 
+     * @param version The new version, may be {@code null} if the version is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setVersion( String version )
+    {
+        this.version = version;
+        return this;
+    }
+
+    /**
+     * Gets the new scope to apply to the dependency.
+     * 
+     * @return The new scope or {@code null} if the scope is not managed and the existing dependency scope should remain
+     *         unchanged.
+     */
+    public String getScope()
+    {
+        return scope;
+    }
+
+    /**
+     * Sets the new scope to apply to the dependency.
+     * 
+     * @param scope The new scope, may be {@code null} if the scope is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setScope( String scope )
+    {
+        this.scope = scope;
+        return this;
+    }
+
+    /**
+     * Gets the new optional flag to apply to the dependency.
+     * 
+     * @return The new optional flag or {@code null} if the flag is not managed and the existing optional flag of the
+     *         dependency should remain unchanged.
+     */
+    public Boolean getOptional()
+    {
+        return optional;
+    }
+
+    /**
+     * Sets the new optional flag to apply to the dependency.
+     * 
+     * @param optional The optional flag, may be {@code null} if the flag is not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setOptional( Boolean optional )
+    {
+        this.optional = optional;
+        return this;
+    }
+
+    /**
+     * Gets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of
+     * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged
+     * with information from dependency management or overridden by it.
+     * 
+     * @return The new exclusions or {@code null} if the exclusions are not managed and the existing dependency
+     *         exclusions should remain unchanged.
+     */
+    public Collection<Exclusion> getExclusions()
+    {
+        return exclusions;
+    }
+
+    /**
+     * Sets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of
+     * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged
+     * with information from dependency management or overridden by it.
+     * 
+     * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setExclusions( Collection<Exclusion> exclusions )
+    {
+        this.exclusions = exclusions;
+        return this;
+    }
+
+    /**
+     * Gets the new properties to apply to the dependency. Note that this map denotes the complete set of properties,
+     * i.e. the dependency manager controls whether any existing properties get merged with the information from
+     * dependency management or overridden by it.
+     * 
+     * @return The new artifact properties or {@code null} if the properties are not managed and the existing properties
+     *         should remain unchanged.
+     */
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+    /**
+     * Sets the new properties to apply to the dependency. Note that this map denotes the complete set of properties,
+     * i.e. the dependency manager controls whether any existing properties get merged with the information from
+     * dependency management or overridden by it.
+     * 
+     * @param properties The new artifact properties, may be {@code null} if the properties are not managed.
+     * @return This management update for chaining, never {@code null}.
+     */
+    public DependencyManagement setProperties( Map<String, String> properties )
+    {
+        this.properties = properties;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManager.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManager.java
new file mode 100644
index 0000000..993e388
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManager.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Applies dependency management to the dependencies of a dependency node.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyManager()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencyManager
+{
+
+    /**
+     * Applies dependency management to the specified dependency.
+     * 
+     * @param dependency The dependency to manage, must not be {@code null}.
+     * @return The management update to apply to the dependency or {@code null} if the dependency is not managed at all.
+     */
+    DependencyManagement manageDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency manager for the specified collection context. When calculating the child manager,
+     * implementors are strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency manager for the dependencies of the target node or {@code null} if dependency management
+     *         should no longer be applied.
+     */
+    DependencyManager deriveChildManager( DependencyCollectionContext context );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencySelector.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencySelector.java
new file mode 100644
index 0000000..b257ffa
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencySelector.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Decides what dependencies to include in the dependency graph.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencySelector()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencySelector
+{
+
+    /**
+     * Decides whether the specified dependency should be included in the dependency graph.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code false} if the dependency should be excluded from the children of the current node, {@code true}
+     *         otherwise.
+     */
+    boolean selectDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency selector for the specified collection context. When calculating the child selector,
+     * implementors are strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency selector for the target node or {@code null} if dependencies should be unconditionally
+     *         included in the sub graph.
+     */
+    DependencySelector deriveChildSelector( DependencyCollectionContext context );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyTraverser.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyTraverser.java
new file mode 100644
index 0000000..be1887b
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyTraverser.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * Decides whether the dependencies of a dependency node should be traversed as well.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getDependencyTraverser()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface DependencyTraverser
+{
+
+    /**
+     * Decides whether the dependencies of the specified dependency should be traversed.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code true} if the dependency graph builder should recurse into the specified dependency and process its
+     *         dependencies, {@code false} otherwise.
+     */
+    boolean traverseDependency( Dependency dependency );
+
+    /**
+     * Derives a dependency traverser that will be used to decide whether the transitive dependencies of the dependency
+     * given in the collection context shall be traversed. When calculating the child traverser, implementors are
+     * strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The dependency traverser for the target node or {@code null} if dependencies should be unconditionally
+     *         traversed in the sub graph.
+     */
+    DependencyTraverser deriveChildTraverser( DependencyCollectionContext context );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java
new file mode 100644
index 0000000..54a7004
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java
@@ -0,0 +1,142 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Thrown in case of an unsolvable conflict between different version constraints for a dependency.
+ */
+public class UnsolvableVersionConflictException
+    extends RepositoryException
+{
+
+    private final transient Collection<String> versions;
+
+    private final transient Collection<? extends List<? extends DependencyNode>> paths;
+
+    /**
+     * Creates a new exception with the specified paths to conflicting nodes in the dependency graph.
+     * 
+     * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}.
+     */
+    public UnsolvableVersionConflictException( Collection<? extends List<? extends DependencyNode>> paths )
+    {
+        super( "Could not resolve version conflict among " + toPaths( paths ) );
+        if ( paths == null )
+        {
+            this.paths = Collections.emptyList();
+            this.versions = Collections.emptyList();
+        }
+        else
+        {
+            this.paths = paths;
+            this.versions = new LinkedHashSet<String>();
+            for ( List<? extends DependencyNode> path : paths )
+            {
+                VersionConstraint constraint = path.get( path.size() - 1 ).getVersionConstraint();
+                if ( constraint != null && constraint.getRange() != null )
+                {
+                    versions.add( constraint.toString() );
+                }
+            }
+        }
+    }
+
+    private static String toPaths( Collection<? extends List<? extends DependencyNode>> paths )
+    {
+        String result = "";
+
+        if ( paths != null )
+        {
+            Collection<String> strings = new LinkedHashSet<String>();
+
+            for ( List<? extends DependencyNode> path : paths )
+            {
+                strings.add( toPath( path ) );
+            }
+
+            result = strings.toString();
+        }
+
+        return result;
+    }
+
+    private static String toPath( List<? extends DependencyNode> path )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+
+        for ( Iterator<? extends DependencyNode> it = path.iterator(); it.hasNext(); )
+        {
+            DependencyNode node = it.next();
+            if ( node.getDependency() == null )
+            {
+                continue;
+            }
+
+            Artifact artifact = node.getDependency().getArtifact();
+            buffer.append( artifact.getGroupId() );
+            buffer.append( ':' ).append( artifact.getArtifactId() );
+            buffer.append( ':' ).append( artifact.getExtension() );
+            if ( artifact.getClassifier().length() > 0 )
+            {
+                buffer.append( ':' ).append( artifact.getClassifier() );
+            }
+            buffer.append( ':' ).append( node.getVersionConstraint() );
+
+            if ( it.hasNext() )
+            {
+                buffer.append( " -> " );
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Gets the paths leading to the conflicting dependencies.
+     * 
+     * @return The (read-only) paths leading to the conflicting dependencies, never {@code null}.
+     */
+    public Collection<? extends List<? extends DependencyNode>> getPaths()
+    {
+        return paths;
+    }
+
+    /**
+     * Gets the conflicting version constraints of the dependency.
+     * 
+     * @return The (read-only) conflicting version constraints, never {@code null}.
+     */
+    public Collection<String> getVersions()
+    {
+        return versions;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilter.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilter.java
new file mode 100644
index 0000000..fb36747
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilter.java
@@ -0,0 +1,135 @@
+package org.eclipse.aether.collection;
+
+/*
+ * 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.
+ */
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Decides which versions matching a version range should actually be considered for the dependency graph. The version
+ * filter is not invoked for dependencies that do not declare a version range but a single version.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ * <p>
+ * <em>Warning:</em> This hook is called from a hot spot and therefore implementations should pay attention to
+ * performance. Among others, implementations should provide a semantic {@link Object#equals(Object) equals()} method.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getVersionFilter()
+ * @see org.eclipse.aether.RepositorySystem#collectDependencies(org.eclipse.aether.RepositorySystemSession,
+ *      CollectRequest)
+ */
+public interface VersionFilter
+{
+
+    /**
+     * A context used during version filtering to hold relevant data.
+     * 
+     * @noimplement This interface is not intended to be implemented by clients.
+     * @noextend This interface is not intended to be extended by clients.
+     */
+    interface VersionFilterContext
+        extends Iterable<Version>
+    {
+
+        /**
+         * Gets the repository system session during which the version filtering happens.
+         * 
+         * @return The repository system session, never {@code null}.
+         */
+        RepositorySystemSession getSession();
+
+        /**
+         * Gets the dependency whose version range is being filtered.
+         * 
+         * @return The dependency, never {@code null}.
+         */
+        Dependency getDependency();
+
+        /**
+         * Gets the total number of available versions. This count reflects any removals made during version filtering.
+         * 
+         * @return The total number of available versions.
+         */
+        int getCount();
+
+        /**
+         * Gets an iterator over the available versions of the dependency. The iterator returns versions in ascending
+         * order. Use {@link Iterator#remove()} to exclude a version from further consideration in the dependency graph.
+         * 
+         * @return The iterator of available versions, never {@code null}.
+         */
+        Iterator<Version> iterator();
+
+        /**
+         * Gets the version constraint that was parsed from the dependency's version string.
+         * 
+         * @return The parsed version constraint, never {@code null}.
+         */
+        VersionConstraint getVersionConstraint();
+
+        /**
+         * Gets the repository from which the specified version was resolved.
+         * 
+         * @param version The version whose source repository should be retrieved, must not be {@code null}.
+         * @return The repository from which the version was resolved or {@code null} if unknown.
+         */
+        ArtifactRepository getRepository( Version version );
+
+        /**
+         * Gets the remote repositories from which the versions were resolved.
+         * 
+         * @return The (read-only) list of repositories, never {@code null}.
+         */
+        List<RemoteRepository> getRepositories();
+
+    }
+
+    /**
+     * Filters the available versions for a given dependency. Implementations will usually call
+     * {@link VersionFilterContext#iterator() context.iterator()} to inspect the available versions and use
+     * {@link java.util.Iterator#remove()} to delete unacceptable versions. If no versions remain after all filtering
+     * has been performed, the dependency collection process will automatically fail, i.e. implementations need not
+     * handle this situation on their own.
+     * 
+     * @param context The version filter context, must not be {@code null}.
+     * @throws RepositoryException If the filtering could not be performed.
+     */
+    void filterVersions( VersionFilterContext context )
+        throws RepositoryException;
+
+    /**
+     * Derives a version filter for the specified collection context. The derived filter will be used to handle version
+     * ranges encountered in child dependencies of the current node. When calculating the child filter, implementors are
+     * strongly advised to simply return the current instance if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code null}.
+     * @return The version filter for the target node or {@code null} if versions should not be filtered any more.
+     */
+    VersionFilter deriveChildFilter( DependencyCollectionContext context );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/package-info.java
new file mode 100644
index 0000000..414629f
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/package-info.java
@@ -0,0 +1,25 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The types and extension points for collecting the transitive dependencies of an artifact and building a dependency
+ * graph.
+ */
+package org.eclipse.aether.collection;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployRequest.java
new file mode 100644
index 0000000..637f47d
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployRequest.java
@@ -0,0 +1,202 @@
+package org.eclipse.aether.deployment;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to deploy artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#deploy(RepositorySystemSession, DeployRequest)
+ */
+public final class DeployRequest
+{
+
+    private Collection<Artifact> artifacts = Collections.emptyList();
+
+    private Collection<Metadata> metadata = Collections.emptyList();
+
+    private RemoteRepository repository;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public DeployRequest()
+    {
+    }
+
+    /**
+     * Gets the artifact to deploy.
+     * 
+     * @return The artifacts to deploy, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts to deploy.
+     * 
+     * @param artifacts The artifacts to deploy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts for deployment.
+     * 
+     * @param artifact The artifact to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata to deploy.
+     * 
+     * @return The metadata to deploy, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to deploy.
+     * 
+     * @param metadata The metadata to deploy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata for deployment.
+     * 
+     * @param metadata The metadata to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repository to deploy to.
+     * 
+     * @return The repository to deploy to or {@code null} if not set.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository to deploy to.
+     * 
+     * @param repository The repository to deploy to, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DeployRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata() + " > " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployResult.java
new file mode 100644
index 0000000..823f671
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeployResult.java
@@ -0,0 +1,171 @@
+package org.eclipse.aether.deployment;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * The result of deploying artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#deploy(RepositorySystemSession, DeployRequest)
+ */
+public final class DeployResult
+{
+
+    private final DeployRequest request;
+
+    private Collection<Artifact> artifacts;
+
+    private Collection<Metadata> metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The deployment request, must not be {@code null}.
+     */
+    public DeployResult( DeployRequest request )
+    {
+        this.request = requireNonNull( request, "deploy request cannot be null" );
+        artifacts = Collections.emptyList();
+        metadata = Collections.emptyList();
+    }
+
+    /**
+     * Gets the deploy request that was made.
+     *
+     * @return The deploy request, never {@code null}.
+     */
+    public DeployRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the artifacts that got deployed.
+     * 
+     * @return The deployed artifacts, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts that got deployed.
+     * 
+     * @param artifacts The deployed artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts to the result.
+     * 
+     * @param artifact The deployed artifact to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata that got deployed. Note that due to automatically generated metadata, there might have been
+     * more metadata deployed than originally specified in the deploy request.
+     * 
+     * @return The deployed metadata, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata that got deployed.
+     * 
+     * @param metadata The deployed metadata, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata to this result.
+     * 
+     * @param metadata The deployed metadata to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DeployResult addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeploymentException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeploymentException.java
new file mode 100644
index 0000000..53252ba
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/DeploymentException.java
@@ -0,0 +1,52 @@
+package org.eclipse.aether.deployment;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a deployment error like authentication failure.
+ */
+public class DeploymentException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public DeploymentException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DeploymentException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/package-info.java
new file mode 100644
index 0000000..dc50c21
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/deployment/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The types supporting the publishing of artifacts to a remote repository.
+ */
+package org.eclipse.aether.deployment;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java
new file mode 100644
index 0000000..ca142fa
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java
@@ -0,0 +1,366 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * A node within a dependency graph.
+ */
+public final class DefaultDependencyNode
+    implements DependencyNode
+{
+
+    private List<DependencyNode> children;
+
+    private Dependency dependency;
+
+    private Artifact artifact;
+
+    private List<? extends Artifact> relocations;
+
+    private Collection<? extends Artifact> aliases;
+
+    private VersionConstraint versionConstraint;
+
+    private Version version;
+
+    private byte managedBits;
+
+    private List<RemoteRepository> repositories;
+
+    private String context;
+
+    private Map<Object, Object> data;
+
+    /**
+     * Creates a new node with the specified dependency.
+     * 
+     * @param dependency The dependency associated with this node, may be {@code null} for a root node.
+     */
+    public DefaultDependencyNode( Dependency dependency )
+    {
+        this.dependency = dependency;
+        artifact = ( dependency != null ) ? dependency.getArtifact() : null;
+        children = new ArrayList<DependencyNode>( 0 );
+        aliases = relocations = Collections.emptyList();
+        repositories = Collections.emptyList();
+        context = "";
+        data = Collections.emptyMap();
+    }
+
+    /**
+     * Creates a new root node with the specified artifact as its label. Note that the new node has no dependency, i.e.
+     * {@link #getDependency()} will return {@code null}. Put differently, the specified artifact will not be subject to
+     * dependency collection/resolution.
+     * 
+     * @param artifact The artifact to use as label for this node, may be {@code null}.
+     */
+    public DefaultDependencyNode( Artifact artifact )
+    {
+        this.artifact = artifact;
+        children = new ArrayList<DependencyNode>( 0 );
+        aliases = relocations = Collections.emptyList();
+        repositories = Collections.emptyList();
+        context = "";
+        data = Collections.emptyMap();
+    }
+
+    /**
+     * Creates a mostly shallow clone of the specified node. The new node has its own copy of any custom data and
+     * initially no children.
+     * 
+     * @param node The node to copy, must not be {@code null}.
+     */
+    public DefaultDependencyNode( DependencyNode node )
+    {
+        dependency = node.getDependency();
+        artifact = node.getArtifact();
+        children = new ArrayList<DependencyNode>( 0 );
+        setAliases( node.getAliases() );
+        setRequestContext( node.getRequestContext() );
+        setManagedBits( node.getManagedBits() );
+        setRelocations( node.getRelocations() );
+        setRepositories( node.getRepositories() );
+        setVersion( node.getVersion() );
+        setVersionConstraint( node.getVersionConstraint() );
+        Map<?, ?> data = node.getData();
+        setData( data.isEmpty() ? null : new HashMap<Object, Object>( data ) );
+    }
+
+    public List<DependencyNode> getChildren()
+    {
+        return children;
+    }
+
+    public void setChildren( List<DependencyNode> children )
+    {
+        if ( children == null )
+        {
+            this.children = new ArrayList<DependencyNode>( 0 );
+        }
+        else
+        {
+            this.children = children;
+        }
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    public void setArtifact( Artifact artifact )
+    {
+        if ( dependency == null )
+        {
+            throw new IllegalStateException( "node does not have a dependency" );
+        }
+        dependency = dependency.setArtifact( artifact );
+        this.artifact = dependency.getArtifact();
+    }
+
+    public List<? extends Artifact> getRelocations()
+    {
+        return relocations;
+    }
+
+    /**
+     * Sets the sequence of relocations that was followed to resolve this dependency's artifact.
+     * 
+     * @param relocations The sequence of relocations, may be {@code null}.
+     */
+    public void setRelocations( List<? extends Artifact> relocations )
+    {
+        if ( relocations == null || relocations.isEmpty() )
+        {
+            this.relocations = Collections.emptyList();
+        }
+        else
+        {
+            this.relocations = relocations;
+        }
+    }
+
+    public Collection<? extends Artifact> getAliases()
+    {
+        return aliases;
+    }
+
+    /**
+     * Sets the known aliases for this dependency's artifact.
+     * 
+     * @param aliases The known aliases, may be {@code null}.
+     */
+    public void setAliases( Collection<? extends Artifact> aliases )
+    {
+        if ( aliases == null || aliases.isEmpty() )
+        {
+            this.aliases = Collections.emptyList();
+        }
+        else
+        {
+            this.aliases = aliases;
+        }
+    }
+
+    public VersionConstraint getVersionConstraint()
+    {
+        return versionConstraint;
+    }
+
+    /**
+     * Sets the version constraint that was parsed from the dependency's version declaration.
+     * 
+     * @param versionConstraint The version constraint for this node, may be {@code null}.
+     */
+    public void setVersionConstraint( VersionConstraint versionConstraint )
+    {
+        this.versionConstraint = versionConstraint;
+    }
+
+    public Version getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the version that was selected for the dependency's target artifact.
+     * 
+     * @param version The parsed version, may be {@code null}.
+     */
+    public void setVersion( Version version )
+    {
+        this.version = version;
+    }
+
+    public void setScope( String scope )
+    {
+        if ( dependency == null )
+        {
+            throw new IllegalStateException( "node does not have a dependency" );
+        }
+        dependency = dependency.setScope( scope );
+    }
+
+    public void setOptional( Boolean optional )
+    {
+        if ( dependency == null )
+        {
+            throw new IllegalStateException( "node does not have a dependency" );
+        }
+        dependency = dependency.setOptional( optional );
+    }
+
+    public int getManagedBits()
+    {
+        return managedBits;
+    }
+
+    /**
+     * Sets a bit field indicating which attributes of this node were subject to dependency management.
+     * 
+     * @param managedBits The bit field indicating the managed attributes or {@code 0} if dependency management wasn't
+     *            applied.
+     */
+    public void setManagedBits( int managedBits )
+    {
+        this.managedBits = (byte) ( managedBits & 0x1F );
+    }
+
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories from which this node's artifact shall be resolved.
+     * 
+     * @param repositories The remote repositories to use for artifact resolution, may be {@code null}.
+     */
+    public void setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null || repositories.isEmpty() )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+    }
+
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    public void setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+    }
+
+    public Map<Object, Object> getData()
+    {
+        return data;
+    }
+
+    public void setData( Map<Object, Object> data )
+    {
+        if ( data == null )
+        {
+            this.data = Collections.emptyMap();
+        }
+        else
+        {
+            this.data = data;
+        }
+    }
+
+    public void setData( Object key, Object value )
+    {
+        requireNonNull( key, "key cannot be null" );
+
+        if ( value == null )
+        {
+            if ( !data.isEmpty() )
+            {
+                data.remove( key );
+
+                if ( data.isEmpty() )
+                {
+                    data = Collections.emptyMap();
+                }
+            }
+        }
+        else
+        {
+            if ( data.isEmpty() )
+            {
+                data = new HashMap<Object, Object>( 1, 2 ); // nodes can be numerous so let's be space conservative
+            }
+            data.put( key, value );
+        }
+    }
+
+    public boolean accept( DependencyVisitor visitor )
+    {
+        if ( visitor.visitEnter( this ) )
+        {
+            for ( DependencyNode child : children )
+            {
+                if ( !child.accept( visitor ) )
+                {
+                    break;
+                }
+            }
+        }
+
+        return visitor.visitLeave( this );
+    }
+
+    @Override
+    public String toString()
+    {
+        Dependency dep = getDependency();
+        if ( dep == null )
+        {
+            return String.valueOf( getArtifact() );
+        }
+        return dep.toString();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Dependency.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Dependency.java
new file mode 100644
index 0000000..2e1a78b
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Dependency.java
@@ -0,0 +1,327 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.NoSuchElementException;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A dependency to some artifact. <em>Note:</em> Instances of this class are immutable and the exposed mutators return
+ * new objects rather than changing the current instance.
+ */
+public final class Dependency
+{
+
+    private final Artifact artifact;
+
+    private final String scope;
+
+    private final Boolean optional;
+
+    private final Set<Exclusion> exclusions;
+
+    /**
+     * Creates a mandatory dependency on the specified artifact with the given scope.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     */
+    public Dependency( Artifact artifact, String scope )
+    {
+        this( artifact, scope, false );
+    }
+
+    /**
+     * Creates a dependency on the specified artifact with the given scope.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
+     */
+    public Dependency( Artifact artifact, String scope, Boolean optional )
+    {
+        this( artifact, scope, optional, null );
+    }
+
+    /**
+     * Creates a dependency on the specified artifact with the given scope and exclusions.
+     * 
+     * @param artifact The artifact being depended on, must not be {@code null}.
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
+     * @param exclusions The exclusions that apply to transitive dependencies, may be {@code null} if none.
+     */
+    public Dependency( Artifact artifact, String scope, Boolean optional, Collection<Exclusion> exclusions )
+    {
+        this( artifact, scope, Exclusions.copy( exclusions ), optional );
+    }
+
+    private Dependency( Artifact artifact, String scope, Set<Exclusion> exclusions, Boolean optional )
+    {
+        // NOTE: This constructor assumes immutability of the provided exclusion collection, for internal use only
+        this.artifact = requireNonNull( artifact, "artifact cannot be null" );
+        this.scope = ( scope != null ) ? scope : "";
+        this.optional = optional;
+        this.exclusions = exclusions;
+    }
+
+    /**
+     * Gets the artifact being depended on.
+     * 
+     * @return The artifact, never {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact being depended on.
+     * 
+     * @param artifact The artifact, must not be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setArtifact( Artifact artifact )
+    {
+        if ( this.artifact.equals( artifact ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Gets the scope of the dependency. The scope defines in which context this dependency is relevant.
+     * 
+     * @return The scope or an empty string if not set, never {@code null}.
+     */
+    public String getScope()
+    {
+        return scope;
+    }
+
+    /**
+     * Sets the scope of the dependency, e.g. "compile".
+     * 
+     * @param scope The scope of the dependency, may be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setScope( String scope )
+    {
+        if ( this.scope.equals( scope ) || ( scope == null && this.scope.length() <= 0 ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Indicates whether this dependency is optional or not. Optional dependencies can be ignored in some contexts.
+     * 
+     * @return {@code true} if the dependency is (definitively) optional, {@code false} otherwise.
+     */
+    public boolean isOptional()
+    {
+        return Boolean.TRUE.equals( optional );
+    }
+
+    /**
+     * Gets the optional flag for the dependency. Note: Most clients will usually call {@link #isOptional()} to
+     * determine the optional flag, this method is for advanced use cases where three-valued logic is required.
+     * 
+     * @return The optional flag or {@code null} if unspecified.
+     */
+    public Boolean getOptional()
+    {
+        return optional;
+    }
+
+    /**
+     * Sets the optional flag for the dependency.
+     * 
+     * @param optional {@code true} if the dependency is optional, {@code false} if the dependency is mandatory, may be
+     *            {@code null} if unspecified.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setOptional( Boolean optional )
+    {
+        if ( eq( this.optional, optional ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, exclusions, optional );
+    }
+
+    /**
+     * Gets the exclusions for this dependency. Exclusions can be used to remove transitive dependencies during
+     * resolution.
+     * 
+     * @return The (read-only) exclusions, never {@code null}.
+     */
+    public Collection<Exclusion> getExclusions()
+    {
+        return exclusions;
+    }
+
+    /**
+     * Sets the exclusions for the dependency.
+     * 
+     * @param exclusions The exclusions, may be {@code null}.
+     * @return The new dependency, never {@code null}.
+     */
+    public Dependency setExclusions( Collection<Exclusion> exclusions )
+    {
+        if ( hasEquivalentExclusions( exclusions ) )
+        {
+            return this;
+        }
+        return new Dependency( artifact, scope, optional, exclusions );
+    }
+
+    private boolean hasEquivalentExclusions( Collection<Exclusion> exclusions )
+    {
+        if ( exclusions == null || exclusions.isEmpty() )
+        {
+            return this.exclusions.isEmpty();
+        }
+        if ( exclusions instanceof Set )
+        {
+            return this.exclusions.equals( exclusions );
+        }
+        return exclusions.size() >= this.exclusions.size() && this.exclusions.containsAll( exclusions )
+            && exclusions.containsAll( this.exclusions );
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getArtifact() ) + " (" + getScope() + ( isOptional() ? "?" : "" ) + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Dependency that = (Dependency) obj;
+
+        return artifact.equals( that.artifact ) && scope.equals( that.scope ) && eq( optional, that.optional )
+            && exclusions.equals( that.exclusions );
+    }
+
+    private static <T> boolean eq( T o1, T o2 )
+    {
+        return ( o1 != null ) ? o1.equals( o2 ) : o2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + artifact.hashCode();
+        hash = hash * 31 + scope.hashCode();
+        hash = hash * 31 + ( optional != null ? optional.hashCode() : 0 );
+        hash = hash * 31 + exclusions.size();
+        return hash;
+    }
+
+    private static class Exclusions
+        extends AbstractSet<Exclusion>
+    {
+
+        private final Exclusion[] exclusions;
+
+        public static Set<Exclusion> copy( Collection<Exclusion> exclusions )
+        {
+            if ( exclusions == null || exclusions.isEmpty() )
+            {
+                return Collections.emptySet();
+            }
+            return new Exclusions( exclusions );
+        }
+
+        private Exclusions( Collection<Exclusion> exclusions )
+        {
+            if ( exclusions.size() > 1 && !( exclusions instanceof Set ) )
+            {
+                exclusions = new LinkedHashSet<Exclusion>( exclusions );
+            }
+            this.exclusions = exclusions.toArray( new Exclusion[exclusions.size()] );
+        }
+
+        @Override
+        public Iterator<Exclusion> iterator()
+        {
+            return new Iterator<Exclusion>()
+            {
+
+                private int cursor = 0;
+
+                public boolean hasNext()
+                {
+                    return cursor < exclusions.length;
+                }
+
+                public Exclusion next()
+                {
+                    try
+                    {
+                        Exclusion exclusion = exclusions[cursor];
+                        cursor++;
+                        return exclusion;
+                    }
+                    catch ( IndexOutOfBoundsException e )
+                    {
+                        throw new NoSuchElementException();
+                    }
+                }
+
+                public void remove()
+                {
+                    throw new UnsupportedOperationException();
+                }
+
+            };
+        }
+
+        @Override
+        public int size()
+        {
+            return exclusions.length;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyCycle.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyCycle.java
new file mode 100644
index 0000000..1076ab8
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyCycle.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+/**
+ * A cycle within a dependency graph, that is a sequence of dependencies d_1, d_2, ..., d_n where d_1 and d_n have the
+ * same versionless coordinates. In more practical terms, a cycle occurs when a project directly or indirectly depends
+ * on its own output artifact.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyCycle
+{
+
+    /**
+     * Gets the dependencies that lead to the first dependency on the cycle, starting from the root of the dependency
+     * graph.
+     * 
+     * @return The (read-only) sequence of dependencies that precedes the cycle in the graph, potentially empty but
+     *         never {@code null}.
+     */
+    List<Dependency> getPrecedingDependencies();
+
+    /**
+     * Gets the dependencies that actually form the cycle. For example, a -&gt; b -&gt; c -&gt; a, i.e. the last
+     * dependency in this sequence duplicates the first element and closes the cycle. Hence the length of the cycle is
+     * the size of the returned sequence minus 1.
+     * 
+     * @return The (read-only) sequence of dependencies that forms the cycle, never {@code null}.
+     */
+    List<Dependency> getCyclicDependencies();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyFilter.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyFilter.java
new file mode 100644
index 0000000..41776ff
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyFilter.java
@@ -0,0 +1,42 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+/**
+ * A filter to include/exclude dependency nodes during other operations.
+ */
+public interface DependencyFilter
+{
+
+    /**
+     * Indicates whether the specified dependency node shall be included or excluded.
+     * 
+     * @param node The dependency node to filter, must not be {@code null}.
+     * @param parents The (read-only) chain of parent nodes that leads to the node to be filtered, must not be
+     *            {@code null}. Iterating this (possibly empty) list walks up the dependency graph towards the root
+     *            node, i.e. the immediate parent node (if any) is the first node in the list. The size of the list also
+     *            denotes the zero-based depth of the filtered node.
+     * @return {@code true} to include the dependency node, {@code false} to exclude it.
+     */
+    boolean accept( DependencyNode node, List<DependencyNode> parents );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java
new file mode 100644
index 0000000..2551043
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java
@@ -0,0 +1,232 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * A node within a dependency graph. To conserve memory, dependency graphs may reuse a given node instance multiple
+ * times to represent reoccurring dependencies. As such clients traversing a dependency graph should be prepared to
+ * discover multiple paths leading to the same node instance unless the input graph is known to be a duplicate-free
+ * tree. <em>Note:</em> Unless otherwise noted, implementation classes are not thread-safe and dependency nodes should
+ * not be mutated by concurrent threads.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface DependencyNode
+{
+
+    /**
+     * A bit flag indicating the dependency version was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_VERSION = 0x01;
+
+    /**
+     * A bit flag indicating the dependency scope was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_SCOPE = 0x02;
+
+    /**
+     * A bit flag indicating the optional flag was subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_OPTIONAL = 0x04;
+
+    /**
+     * A bit flag indicating the artifact properties were subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_PROPERTIES = 0x08;
+
+    /**
+     * A bit flag indicating the exclusions were subject to dependency management
+     * 
+     * @see #getManagedBits()
+     */
+    int MANAGED_EXCLUSIONS = 0x10;
+
+    /**
+     * Gets the child nodes of this node. To conserve memory, dependency nodes with equal dependencies may share the
+     * same child list instance. Hence clients mutating the child list need to be aware that these changes might affect
+     * more than this node. Where this is not desired, the child list should be copied before mutation if the client
+     * cannot be sure whether it might be shared with other nodes in the graph.
+     * 
+     * @return The child nodes of this node, never {@code null}.
+     */
+    List<DependencyNode> getChildren();
+
+    /**
+     * Sets the child nodes of this node.
+     * 
+     * @param children The child nodes, may be {@code null}
+     */
+    void setChildren( List<DependencyNode> children );
+
+    /**
+     * Gets the dependency associated with this node. <em>Note:</em> For dependency graphs that have been constructed
+     * without a root dependency, this method will yield {@code null} when invoked on the graph's root node. The root
+     * node of such graphs may however still have a label as returned by {@link #getArtifact()}.
+     * 
+     * @return The dependency or {@code null} if none.
+     */
+    Dependency getDependency();
+
+    /**
+     * Gets the artifact associated with this node. If this node is associated with a dependency, this is equivalent to
+     * {@code getDependency().getArtifact()}. Otherwise the artifact merely provides a label for this node in which case
+     * the artifact must not be subjected to dependency collection/resolution.
+     * 
+     * @return The associated artifact or {@code null} if none.
+     */
+    Artifact getArtifact();
+
+    /**
+     * Updates the artifact of the dependency after resolution. The new artifact must have the same coordinates as the
+     * original artifact. This method may only be invoked if this node actually has a dependency, i.e. if
+     * {@link #getDependency()} is not null.
+     * 
+     * @param artifact The artifact satisfying the dependency, must not be {@code null}.
+     */
+    void setArtifact( Artifact artifact );
+
+    /**
+     * Gets the sequence of relocations that was followed to resolve the artifact referenced by the dependency.
+     * 
+     * @return The (read-only) sequence of relocations, never {@code null}.
+     */
+    List<? extends Artifact> getRelocations();
+
+    /**
+     * Gets the known aliases for this dependency's artifact. An alias can be used to mark a patched rebuild of some
+     * other artifact as such, thereby allowing conflict resolution to consider the patched and the original artifact as
+     * a conflict.
+     * 
+     * @return The (read-only) set of known aliases, never {@code null}.
+     */
+    Collection<? extends Artifact> getAliases();
+
+    /**
+     * Gets the version constraint that was parsed from the dependency's version declaration.
+     * 
+     * @return The version constraint for this node or {@code null}.
+     */
+    VersionConstraint getVersionConstraint();
+
+    /**
+     * Gets the version that was selected for the dependency's target artifact.
+     * 
+     * @return The parsed version or {@code null}.
+     */
+    Version getVersion();
+
+    /**
+     * Sets the scope of the dependency. This method may only be invoked if this node actually has a dependency, i.e. if
+     * {@link #getDependency()} is not null.
+     * 
+     * @param scope The scope, may be {@code null}.
+     */
+    void setScope( String scope );
+
+    /**
+     * Sets the optional flag of the dependency. This method may only be invoked if this node actually has a dependency,
+     * i.e. if {@link #getDependency()} is not null.
+     * 
+     * @param optional The optional flag, may be {@code null}.
+     */
+    void setOptional( Boolean optional );
+
+    /**
+     * Gets a bit field indicating which attributes of this node were subject to dependency management.
+     * 
+     * @return A bit field containing any of the bits {@link #MANAGED_VERSION}, {@link #MANAGED_SCOPE},
+     *         {@link #MANAGED_OPTIONAL}, {@link #MANAGED_PROPERTIES} and {@link #MANAGED_EXCLUSIONS} if the
+     *         corresponding attribute was set via dependency management.
+     */
+    int getManagedBits();
+
+    /**
+     * Gets the remote repositories from which this node's artifact shall be resolved.
+     * 
+     * @return The (read-only) list of remote repositories to use for artifact resolution, never {@code null}.
+     */
+    List<RemoteRepository> getRepositories();
+
+    /**
+     * Gets the request context in which this dependency node was created.
+     * 
+     * @return The request context, never {@code null}.
+     */
+    String getRequestContext();
+
+    /**
+     * Sets the request context in which this dependency node was created.
+     * 
+     * @param context The context, may be {@code null}.
+     */
+    void setRequestContext( String context );
+
+    /**
+     * Gets the custom data associated with this dependency node. Clients of the repository system can use this data to
+     * annotate dependency nodes with domain-specific information. Note that the returned map is read-only and
+     * {@link #setData(Object, Object)} needs to be used to update the custom data.
+     * 
+     * @return The (read-only) key-value mappings, never {@code null}.
+     */
+    Map<?, ?> getData();
+
+    /**
+     * Sets the custom data associated with this dependency node.
+     * 
+     * @param data The new custom data, may be {@code null}.
+     */
+    void setData( Map<Object, Object> data );
+
+    /**
+     * Associates the specified dependency node data with the given key. <em>Note:</em> This method must not be called
+     * while {@link #getData()} is being iterated.
+     * 
+     * @param key The key under which to store the data, must not be {@code null}.
+     * @param value The data to associate with the key, may be {@code null} to remove the mapping.
+     */
+    void setData( Object key, Object value );
+
+    /**
+     * Traverses this node and potentially its children using the specified visitor.
+     * 
+     * @param visitor The visitor to call back, must not be {@code null}.
+     * @return {@code true} to visit siblings nodes of this node as well, {@code false} to skip siblings.
+     */
+    boolean accept( DependencyVisitor visitor );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyVisitor.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyVisitor.java
new file mode 100644
index 0000000..2a85f2d
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyVisitor.java
@@ -0,0 +1,47 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+/**
+ * A visitor for nodes of the dependency graph.
+ * 
+ * @see DependencyNode#accept(DependencyVisitor)
+ */
+public interface DependencyVisitor
+{
+
+    /**
+     * Notifies the visitor of a node visit before its children have been processed.
+     * 
+     * @param node The dependency node being visited, must not be {@code null}.
+     * @return {@code true} to visit child nodes of the specified node as well, {@code false} to skip children.
+     */
+    boolean visitEnter( DependencyNode node );
+
+    /**
+     * Notifies the visitor of a node visit after its children have been processed. Note that this method is always
+     * invoked regardless whether any children have actually been visited.
+     * 
+     * @param node The dependency node being visited, must not be {@code null}.
+     * @return {@code true} to visit siblings nodes of the specified node as well, {@code false} to skip siblings.
+     */
+    boolean visitLeave( DependencyNode node );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Exclusion.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Exclusion.java
new file mode 100644
index 0000000..497cf43
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/Exclusion.java
@@ -0,0 +1,131 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+/**
+ * An exclusion of one or more transitive dependencies. <em>Note:</em> Instances of this class are immutable and the
+ * exposed mutators return new objects rather than changing the current instance.
+ * 
+ * @see Dependency#getExclusions()
+ */
+public final class Exclusion
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String classifier;
+
+    private final String extension;
+
+    /**
+     * Creates an exclusion for artifacts with the specified coordinates.
+     * 
+     * @param groupId The group identifier, may be {@code null}.
+     * @param artifactId The artifact identifier, may be {@code null}.
+     * @param classifier The classifier, may be {@code null}.
+     * @param extension The file extension, may be {@code null}.
+     */
+    public Exclusion( String groupId, String artifactId, String classifier, String extension )
+    {
+        this.groupId = ( groupId != null ) ? groupId : "";
+        this.artifactId = ( artifactId != null ) ? artifactId : "";
+        this.classifier = ( classifier != null ) ? classifier : "";
+        this.extension = ( extension != null ) ? extension : "";
+    }
+
+    /**
+     * Gets the group identifier for artifacts to exclude.
+     * 
+     * @return The group identifier, never {@code null}.
+     */
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    /**
+     * Gets the artifact identifier for artifacts to exclude.
+     * 
+     * @return The artifact identifier, never {@code null}.
+     */
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    /**
+     * Gets the classifier for artifacts to exclude.
+     * 
+     * @return The classifier, never {@code null}.
+     */
+    public String getClassifier()
+    {
+        return classifier;
+    }
+
+    /**
+     * Gets the file extension for artifacts to exclude.
+     * 
+     * @return The file extension of artifacts to exclude, never {@code null}.
+     */
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getGroupId() + ':' + getArtifactId() + ':' + getExtension()
+            + ( getClassifier().length() > 0 ? ':' + getClassifier() : "" );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Exclusion that = (Exclusion) obj;
+
+        return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId )
+            && extension.equals( that.extension ) && classifier.equals( that.classifier );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + artifactId.hashCode();
+        hash = hash * 31 + groupId.hashCode();
+        hash = hash * 31 + classifier.hashCode();
+        hash = hash * 31 + extension.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/package-info.java
new file mode 100644
index 0000000..c3ba9db
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The representation of a dependency graph by means of connected dependency nodes.
+ */
+package org.eclipse.aether.graph;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallRequest.java
new file mode 100644
index 0000000..f9b3163
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallRequest.java
@@ -0,0 +1,177 @@
+package org.eclipse.aether.installation;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A request to install artifacts and their accompanying metadata into the local repository.
+ * 
+ * @see RepositorySystem#install(RepositorySystemSession, InstallRequest)
+ */
+public final class InstallRequest
+{
+
+    private Collection<Artifact> artifacts = Collections.emptyList();
+
+    private Collection<Metadata> metadata = Collections.emptyList();
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public InstallRequest()
+    {
+    }
+
+    /**
+     * Gets the artifact to install.
+     * 
+     * @return The artifacts to install, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts to install.
+     * 
+     * @param artifacts The artifacts to install, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts for installation.
+     * 
+     * @param artifact The artifact to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata to install.
+     * 
+     * @return The metadata to install, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to install.
+     * 
+     * @param metadata The metadata to install.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata for installation.
+     * 
+     * @param metadata The metadata to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public InstallRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallResult.java
new file mode 100644
index 0000000..2f79023
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallResult.java
@@ -0,0 +1,171 @@
+package org.eclipse.aether.installation;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * The result of installing artifacts and their accompanying metadata into the a remote repository.
+ * 
+ * @see RepositorySystem#install(RepositorySystemSession, InstallRequest)
+ */
+public final class InstallResult
+{
+
+    private final InstallRequest request;
+
+    private Collection<Artifact> artifacts;
+
+    private Collection<Metadata> metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The installation request, must not be {@code null}.
+     */
+    public InstallResult( InstallRequest request )
+    {
+        this.request = requireNonNull( request, "install request cannot be null" );
+        artifacts = Collections.emptyList();
+        metadata = Collections.emptyList();
+    }
+
+    /**
+     * Gets the install request that was made.
+     *
+     * @return The install request, never {@code null}.
+     */
+    public InstallRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the artifacts that got installed.
+     * 
+     * @return The installed artifacts, never {@code null}.
+     */
+    public Collection<Artifact> getArtifacts()
+    {
+        return artifacts;
+    }
+
+    /**
+     * Sets the artifacts that got installed.
+     * 
+     * @param artifacts The installed artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult setArtifacts( Collection<Artifact> artifacts )
+    {
+        if ( artifacts == null )
+        {
+            this.artifacts = Collections.emptyList();
+        }
+        else
+        {
+            this.artifacts = artifacts;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified artifacts to the result.
+     * 
+     * @param artifact The installed artifact to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult addArtifact( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( artifacts.isEmpty() )
+            {
+                artifacts = new ArrayList<Artifact>();
+            }
+            artifacts.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the metadata that got installed. Note that due to automatically generated metadata, there might have been
+     * more metadata installed than originally specified in the install request.
+     * 
+     * @return The installed metadata, never {@code null}.
+     */
+    public Collection<Metadata> getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata that got installed.
+     * 
+     * @param metadata The installed metadata, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult setMetadata( Collection<Metadata> metadata )
+    {
+        if ( metadata == null )
+        {
+            this.metadata = Collections.emptyList();
+        }
+        else
+        {
+            this.metadata = metadata;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified metadata to this result.
+     * 
+     * @param metadata The installed metadata to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public InstallResult addMetadata( Metadata metadata )
+    {
+        if ( metadata != null )
+        {
+            if ( this.metadata.isEmpty() )
+            {
+                this.metadata = new ArrayList<Metadata>();
+            }
+            this.metadata.add( metadata );
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifacts() + ", " + getMetadata();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallationException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallationException.java
new file mode 100644
index 0000000..9a556bb
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/InstallationException.java
@@ -0,0 +1,52 @@
+package org.eclipse.aether.installation;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an installation error like an IO error.
+ */
+public class InstallationException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public InstallationException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InstallationException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/installation/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/package-info.java
new file mode 100644
index 0000000..d4ac077
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/installation/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The types supporting the publishing of artifacts to a local repository.
+ */
+package org.eclipse.aether.installation;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java
new file mode 100644
index 0000000..49dab35
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java
@@ -0,0 +1,160 @@
+package org.eclipse.aether.metadata;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A skeleton class for metadata.
+ */
+public abstract class AbstractMetadata
+    implements Metadata
+{
+
+    private Metadata newInstance( Map<String, String> properties, File file )
+    {
+        return new DefaultMetadata( getGroupId(), getArtifactId(), getVersion(), getType(), getNature(), file,
+                                    properties );
+    }
+
+    public Metadata setFile( File file )
+    {
+        File current = getFile();
+        if ( ( current == null ) ? file == null : current.equals( file ) )
+        {
+            return this;
+        }
+        return newInstance( getProperties(), file );
+    }
+
+    public Metadata setProperties( Map<String, String> properties )
+    {
+        Map<String, String> current = getProperties();
+        if ( current.equals( properties ) || ( properties == null && current.isEmpty() ) )
+        {
+            return this;
+        }
+        return newInstance( copyProperties( properties ), getFile() );
+    }
+
+    public String getProperty( String key, String defaultValue )
+    {
+        String value = getProperties().get( key );
+        return ( value != null ) ? value : defaultValue;
+    }
+
+    /**
+     * Copies the specified metadata properties. This utility method should be used when creating new metadata instances
+     * with caller-supplied properties.
+     * 
+     * @param properties The properties to copy, may be {@code null}.
+     * @return The copied and read-only properties, never {@code null}.
+     */
+    protected static Map<String, String> copyProperties( Map<String, String> properties )
+    {
+        if ( properties != null && !properties.isEmpty() )
+        {
+            return Collections.unmodifiableMap( new HashMap<String, String>( properties ) );
+        }
+        else
+        {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+        if ( getGroupId().length() > 0 )
+        {
+            buffer.append( getGroupId() );
+        }
+        if ( getArtifactId().length() > 0 )
+        {
+            buffer.append( ':' ).append( getArtifactId() );
+        }
+        if ( getVersion().length() > 0 )
+        {
+            buffer.append( ':' ).append( getVersion() );
+        }
+        buffer.append( '/' ).append( getType() );
+        return buffer.toString();
+    }
+
+    /**
+     * Compares this metadata with the specified object.
+     * 
+     * @param obj The object to compare this metadata against, may be {@code null}.
+     * @return {@code true} if and only if the specified object is another {@link Metadata} with equal coordinates,
+     *         type, nature, properties and file, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( !( obj instanceof Metadata ) )
+        {
+            return false;
+        }
+
+        Metadata that = (Metadata) obj;
+
+        return getArtifactId().equals( that.getArtifactId() ) && getGroupId().equals( that.getGroupId() )
+            && getVersion().equals( that.getVersion() ) && getType().equals( that.getType() )
+            && getNature().equals( that.getNature() ) && eq( getFile(), that.getFile() )
+            && eq( getProperties(), that.getProperties() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    /**
+     * Returns a hash code for this metadata.
+     * 
+     * @return A hash code for the metadata.
+     */
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getGroupId().hashCode();
+        hash = hash * 31 + getArtifactId().hashCode();
+        hash = hash * 31 + getType().hashCode();
+        hash = hash * 31 + getNature().hashCode();
+        hash = hash * 31 + getVersion().hashCode();
+        hash = hash * 31 + hash( getFile() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return ( obj != null ) ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java
new file mode 100644
index 0000000..16bcd61
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java
@@ -0,0 +1,189 @@
+package org.eclipse.aether.metadata;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A basic metadata instance. <em>Note:</em> Instances of this class are immutable and the exposed mutators return new
+ * objects rather than changing the current instance.
+ */
+public final class DefaultMetadata
+    extends AbstractMetadata
+{
+
+    private final String groupId;
+
+    private final String artifactId;
+
+    private final String version;
+
+    private final String type;
+
+    private final Nature nature;
+
+    private final File file;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new metadata for the repository root with the specific type and nature.
+     * 
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String type, Nature nature )
+    {
+        this( "", "", "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String type, Nature nature )
+    {
+        this( groupId, "", "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String type, Nature nature )
+    {
+        this( groupId, artifactId, "", type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature )
+    {
+        this( groupId, artifactId, version, type, nature, null, (File) null );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param file The resolved file of the metadata, may be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature, File file )
+    {
+        this( groupId, artifactId, version, type, nature, null, file );
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     * 
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param properties The properties of the metadata, may be {@code null} if none.
+     * @param file The resolved file of the metadata, may be {@code null}.
+     */
+    public DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature,
+                            Map<String, String> properties, File file )
+    {
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.version = emptify( version );
+        this.type = emptify( type );
+        this.nature = requireNonNull( nature, "metadata nature cannot be null" );
+        this.file = file;
+        this.properties = copyProperties( properties );
+    }
+
+    DefaultMetadata( String groupId, String artifactId, String version, String type, Nature nature, File file,
+                     Map<String, String> properties )
+    {
+        // NOTE: This constructor assumes immutability of the provided properties, for internal use only
+        this.groupId = emptify( groupId );
+        this.artifactId = emptify( artifactId );
+        this.version = emptify( version );
+        this.type = emptify( type );
+        this.nature = nature;
+        this.file = file;
+        this.properties = properties;
+    }
+
+    private static String emptify( String str )
+    {
+        return ( str == null ) ? "" : str;
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public String getType()
+    {
+        return type;
+    }
+
+    public Nature getNature()
+    {
+        return nature;
+    }
+
+    public File getFile()
+    {
+        return file;
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java
new file mode 100644
index 0000000..deaff70
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.metadata;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * A piece of metadata that needs to be merged with any current metadata before installation/deployment.
+ */
+public interface MergeableMetadata
+    extends Metadata
+{
+
+    /**
+     * Merges this metadata into the current metadata (if any). Note that this method will be invoked regardless whether
+     * metadata currently exists or not.
+     * 
+     * @param current The path to the current metadata file, may not exist but must not be {@code null}.
+     * @param result The path to the result file where the merged metadata should be stored, must not be {@code null}.
+     * @throws RepositoryException If the metadata could not be merged.
+     */
+    void merge( File current, File result )
+        throws RepositoryException;
+
+    /**
+     * Indicates whether this metadata has been merged.
+     * 
+     * @return {@code true} if the metadata has been merged, {@code false} otherwise.
+     */
+    boolean isMerged();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java
new file mode 100644
index 0000000..84e9212
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java
@@ -0,0 +1,138 @@
+package org.eclipse.aether.metadata;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A piece of repository metadata, e.g. an index of available versions. In contrast to an artifact, which usually exists
+ * in only one repository, metadata usually exists in multiple repositories and each repository contains a different
+ * copy of the metadata. <em>Note:</em> Metadata instances are supposed to be immutable, e.g. any exposed mutator method
+ * returns a new metadata instance and leaves the original instance unchanged. Implementors are strongly advised to obey
+ * this contract. <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractMetadata} instead of
+ * directly implementing this interface.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface Metadata
+{
+
+    /**
+     * The nature of the metadata.
+     */
+    enum Nature
+    {
+        /**
+         * The metadata refers to release artifacts only.
+         */
+        RELEASE,
+
+        /**
+         * The metadata refers to snapshot artifacts only.
+         */
+        SNAPSHOT,
+
+        /**
+         * The metadata refers to either release or snapshot artifacts.
+         */
+        RELEASE_OR_SNAPSHOT
+    }
+
+    /**
+     * Gets the group identifier of this metadata.
+     * 
+     * @return The group identifier or an empty string if the metadata applies to the entire repository, never
+     *         {@code null}.
+     */
+    String getGroupId();
+
+    /**
+     * Gets the artifact identifier of this metadata.
+     * 
+     * @return The artifact identifier or an empty string if the metadata applies to the groupId level only, never
+     *         {@code null}.
+     */
+    String getArtifactId();
+
+    /**
+     * Gets the version of this metadata.
+     * 
+     * @return The version or an empty string if the metadata applies to the groupId:artifactId level only, never
+     *         {@code null}.
+     */
+    String getVersion();
+
+    /**
+     * Gets the type of the metadata, e.g. "maven-metadata.xml".
+     * 
+     * @return The type of the metadata, never {@code null}.
+     */
+    String getType();
+
+    /**
+     * Gets the nature of this metadata. The nature indicates to what artifact versions the metadata refers.
+     * 
+     * @return The nature, never {@code null}.
+     */
+    Nature getNature();
+
+    /**
+     * Gets the file of this metadata. Note that only resolved metadata has a file associated with it.
+     * 
+     * @return The file or {@code null} if none.
+     */
+    File getFile();
+
+    /**
+     * Sets the file of the metadata.
+     * 
+     * @param file The file of the metadata, may be {@code null}
+     * @return The new metadata, never {@code null}.
+     */
+    Metadata setFile( File file );
+
+    /**
+     * Gets the specified property.
+     * 
+     * @param key The name of the property, must not be {@code null}.
+     * @param defaultValue The default value to return in case the property is not set, may be {@code null}.
+     * @return The requested property value or {@code null} if the property is not set and no default value was
+     *         provided.
+     */
+    String getProperty( String key, String defaultValue );
+
+    /**
+     * Gets the properties of this metadata.
+     * 
+     * @return The (read-only) properties, never {@code null}.
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * Sets the properties for the metadata.
+     * 
+     * @param properties The properties for the metadata, may be {@code null}.
+     * @return The new metadata, never {@code null}.
+     */
+    Metadata setProperties( Map<String, String> properties );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/package-info.java
new file mode 100644
index 0000000..e41f98e
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The definition of metadata, that is an auxiliary entity managed by the repository system to locate artifacts.
+ */
+package org.eclipse.aether.metadata;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/package-info.java
new file mode 100644
index 0000000..8d11fa8
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The primary API of the {@link org.eclipse.aether.RepositorySystem} and its functionality.
+ */
+package org.eclipse.aether;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ArtifactRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ArtifactRepository.java
new file mode 100644
index 0000000..c62bf87
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ArtifactRepository.java
@@ -0,0 +1,45 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * A repository hosting artifacts.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface ArtifactRepository
+{
+
+    /**
+     * Gets the type of the repository, for example "default".
+     * 
+     * @return The (case-sensitive) type of the repository, never {@code null}.
+     */
+    String getContentType();
+
+    /**
+     * Gets the identifier of this repository.
+     * 
+     * @return The (case-sensitive) identifier, never {@code null}.
+     */
+    String getId();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Authentication.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Authentication.java
new file mode 100644
index 0000000..d85c2a2
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Authentication.java
@@ -0,0 +1,55 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+
+/**
+ * The authentication to use for accessing a protected resource. This acts basically as an extensible callback mechanism
+ * from which network operations can request authentication data like username and password when needed.
+ */
+public interface Authentication
+{
+
+    /**
+     * Fills the given authentication context with the data from this authentication callback. To do so, implementors
+     * have to call {@link AuthenticationContext#put(String, Object)}. <br>
+     * <br>
+     * The {@code key} parameter supplied to this method acts merely as a hint for interactive callbacks that want to
+     * prompt the user for only that authentication data which is required. Implementations are free to ignore this
+     * parameter and put all the data they have into the authentication context at once.
+     * 
+     * @param context The authentication context to populate, must not be {@code null}.
+     * @param key The key denoting a specific piece of authentication data that is being requested for a network
+     *            operation, may be {@code null}.
+     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
+     *            authentication data, may be {@code null}.
+     */
+    void fill( AuthenticationContext context, String key, Map<String, String> data );
+
+    /**
+     * Updates the given digest with data from this authentication callback. To do so, implementors have to call the
+     * {@code update()} methods in {@link AuthenticationDigest}.
+     * 
+     * @param digest The digest to update, must not be {@code null}.
+     */
+    void digest( AuthenticationDigest digest );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationContext.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationContext.java
new file mode 100644
index 0000000..93bcdce
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationContext.java
@@ -0,0 +1,390 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A glorified map of key value pairs holding (cleartext) authentication data. Authentication contexts are used
+ * internally when network operations need to access secured repositories or proxies. Each authentication context
+ * manages the credentials required to access a single host. Unlike {@link Authentication} callbacks which exist for a
+ * potentially long time like the duration of a repository system session, an authentication context has a supposedly
+ * short lifetime and should be {@link #close() closed} as soon as the corresponding network operation has finished:
+ * 
+ * <pre>
+ * AuthenticationContext context = AuthenticationContext.forRepository( session, repository );
+ * try {
+ *     // get credentials
+ *     char[] password = context.get( AuthenticationContext.PASSWORD, char[].class );
+ *     // perform network operation using retrieved credentials
+ *     ...
+ * } finally {
+ *     // erase confidential authentication data from heap memory
+ *     AuthenticationContext.close( context );
+ * }
+ * </pre>
+ * 
+ * The same authentication data can often be presented using different data types, e.g. a password can be presented
+ * using a character array or (less securely) using a string. For ease of use, an authentication context treats the
+ * following groups of data types as equivalent and converts values automatically during retrieval:
+ * <ul>
+ * <li>{@code String}, {@code char[]}</li>
+ * <li>{@code String}, {@code File}</li>
+ * </ul>
+ * An authentication context is thread-safe.
+ */
+public final class AuthenticationContext
+    implements Closeable
+{
+
+    /**
+     * The key used to store the username. The corresponding authentication data should be of type {@link String}.
+     */
+    public static final String USERNAME = "username";
+
+    /**
+     * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
+     * {@link String}.
+     */
+    public static final String PASSWORD = "password";
+
+    /**
+     * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
+     */
+    public static final String NTLM_DOMAIN = "ntlm.domain";
+
+    /**
+     * The key used to store the NTML workstation. The corresponding authentication data should be of type
+     * {@link String}.
+     */
+    public static final String NTLM_WORKSTATION = "ntlm.workstation";
+
+    /**
+     * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
+     * {@link String} or {@link File}.
+     */
+    public static final String PRIVATE_KEY_PATH = "privateKey.path";
+
+    /**
+     * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
+     * of type {@code char[]} or {@link String}.
+     */
+    public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
+
+    /**
+     * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
+     * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
+     * {@link #HOST_KEY_REMOTE} and {@link #HOST_KEY_LOCAL}, e.g. to enable a well-founded decision of the user during
+     * an interactive prompt.
+     */
+    public static final String HOST_KEY_ACCEPTANCE = "hostKey.acceptance";
+
+    /**
+     * The key used to store the fingerprint of the public key advertised by remote host. Note that this key is used to
+     * query the extra data passed to {@link #get(String, Map, Class)} when getting {@link #HOST_KEY_ACCEPTANCE}, not
+     * the authentication data in a context.
+     */
+    public static final String HOST_KEY_REMOTE = "hostKey.remote";
+
+    /**
+     * The key used to store the fingerprint of the public key expected from remote host as recorded in a known hosts
+     * database. Note that this key is used to query the extra data passed to {@link #get(String, Map, Class)} when
+     * getting {@link #HOST_KEY_ACCEPTANCE}, not the authentication data in a context.
+     */
+    public static final String HOST_KEY_LOCAL = "hostKey.local";
+
+    /**
+     * The key used to store the SSL context. The corresponding authentication data should be of type
+     * {@link javax.net.ssl.SSLContext}.
+     */
+    public static final String SSL_CONTEXT = "ssl.context";
+
+    /**
+     * The key used to store the SSL hostname verifier. The corresponding authentication data should be of type
+     * {@link javax.net.ssl.HostnameVerifier}.
+     */
+    public static final String SSL_HOSTNAME_VERIFIER = "ssl.hostnameVerifier";
+
+    private final RepositorySystemSession session;
+
+    private final RemoteRepository repository;
+
+    private final Proxy proxy;
+
+    private final Authentication auth;
+
+    private final Map<String, Object> authData;
+
+    private boolean fillingAuthData;
+
+    /**
+     * Gets an authentication context for the specified repository.
+     * 
+     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
+     * @param repository The repository for which to create an authentication context, must not be {@code null}.
+     * @return An authentication context for the repository or {@code null} if no authentication is configured for it.
+     */
+    public static AuthenticationContext forRepository( RepositorySystemSession session, RemoteRepository repository )
+    {
+        return newInstance( session, repository, null, repository.getAuthentication() );
+    }
+
+    /**
+     * Gets an authentication context for the proxy of the specified repository.
+     * 
+     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
+     * @param repository The repository for whose proxy to create an authentication context, must not be {@code null}.
+     * @return An authentication context for the proxy or {@code null} if no proxy is set or no authentication is
+     *         configured for it.
+     */
+    public static AuthenticationContext forProxy( RepositorySystemSession session, RemoteRepository repository )
+    {
+        Proxy proxy = repository.getProxy();
+        return newInstance( session, repository, proxy, ( proxy != null ) ? proxy.getAuthentication() : null );
+    }
+
+    private static AuthenticationContext newInstance( RepositorySystemSession session, RemoteRepository repository,
+                                                      Proxy proxy, Authentication auth )
+    {
+        if ( auth == null )
+        {
+            return null;
+        }
+        return new AuthenticationContext( session, repository, proxy, auth );
+    }
+
+    private AuthenticationContext( RepositorySystemSession session, RemoteRepository repository, Proxy proxy,
+                                   Authentication auth )
+    {
+        this.session = requireNonNull( session, "repository system session cannot be null" );
+        this.repository = repository;
+        this.proxy = proxy;
+        this.auth = auth;
+        authData = new HashMap<String, Object>();
+    }
+
+    /**
+     * Gets the repository system session during which the authentication happens.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
+     * this authentication context does not apply to the repository's host but rather the proxy.
+     * 
+     * @return The repository to be contacted, never {@code null}.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the proxy (if any) to be authenticated with.
+     * 
+     * @return The proxy or {@code null} if authenticating directly with the repository's host.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none.
+     */
+    public String get( String key )
+    {
+        return get( key, null, String.class );
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param <T> The data type of the authentication data.
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @param type The expected type of the authentication data, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
+     */
+    public <T> T get( String key, Class<T> type )
+    {
+        return get( key, null, type );
+    }
+
+    /**
+     * Gets the authentication data for the specified key.
+     * 
+     * @param <T> The data type of the authentication data.
+     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
+     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
+     *            authentication data, may be {@code null}.
+     * @param type The expected type of the authentication data, must not be {@code null}.
+     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
+     */
+    public <T> T get( String key, Map<String, String> data, Class<T> type )
+    {
+        requireNonNull( key, "authentication key cannot be null" );
+        if ( key.length() == 0 )
+        {
+            throw new IllegalArgumentException( "authentication key cannot be empty" );
+        }
+
+        Object value;
+        synchronized ( authData )
+        {
+            value = authData.get( key );
+            if ( value == null && !authData.containsKey( key ) && !fillingAuthData )
+            {
+                if ( auth != null )
+                {
+                    try
+                    {
+                        fillingAuthData = true;
+                        auth.fill( this, key, data );
+                    }
+                    finally
+                    {
+                        fillingAuthData = false;
+                    }
+                    value = authData.get( key );
+                }
+                if ( value == null )
+                {
+                    authData.put( key, value );
+                }
+            }
+        }
+
+        return convert( value, type );
+    }
+
+    private <T> T convert( Object value, Class<T> type )
+    {
+        if ( !type.isInstance( value ) )
+        {
+            if ( String.class.equals( type ) )
+            {
+                if ( value instanceof File )
+                {
+                    value = ( (File) value ).getPath();
+                }
+                else if ( value instanceof char[] )
+                {
+                    value = new String( (char[]) value );
+                }
+            }
+            else if ( File.class.equals( type ) )
+            {
+                if ( value instanceof String )
+                {
+                    value = new File( (String) value );
+                }
+            }
+            else if ( char[].class.equals( type ) )
+            {
+                if ( value instanceof String )
+                {
+                    value = ( (String) value ).toCharArray();
+                }
+            }
+        }
+
+        if ( type.isInstance( value ) )
+        {
+            return type.cast( value );
+        }
+
+        return null;
+    }
+
+    /**
+     * Puts the specified authentication data into this context. This method should only be called from implementors of
+     * {@link Authentication#fill(AuthenticationContext, String, Map)}. Passed in character arrays are not cloned and
+     * become owned by this context, i.e. get erased when the context gets closed.
+     *
+     * @param key The key to associate the authentication data with, must not be {@code null}.
+     * @param value The (cleartext) authentication data to store, may be {@code null}.
+     */
+    public void put( String key, Object value )
+    {
+        requireNonNull( key, "authentication key cannot be null" );
+        if ( key.length() == 0 )
+        {
+            throw new IllegalArgumentException( "authentication key cannot be empty" );
+        }
+
+        synchronized ( authData )
+        {
+            Object oldValue = authData.put( key, value );
+            if ( oldValue instanceof char[] )
+            {
+                Arrays.fill( (char[]) oldValue, '\0' );
+            }
+        }
+    }
+
+    /**
+     * Closes this authentication context and erases sensitive authentication data from heap memory. Closing an already
+     * closed context has no effect.
+     */
+    public void close()
+    {
+        synchronized ( authData )
+        {
+            for ( Object value : authData.values() )
+            {
+                if ( value instanceof char[] )
+                {
+                    Arrays.fill( (char[]) value, '\0' );
+                }
+            }
+            authData.clear();
+        }
+    }
+
+    /**
+     * Closes the specified authentication context. This is a convenience method doing a {@code null} check before
+     * calling {@link #close()} on the given context.
+     * 
+     * @param context The authentication context to close, may be {@code null}.
+     */
+    public static void close( AuthenticationContext context )
+    {
+        if ( context != null )
+        {
+            context.close();
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationDigest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationDigest.java
new file mode 100644
index 0000000..e186060
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationDigest.java
@@ -0,0 +1,212 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A helper to calculate a fingerprint/digest for the authentication data of a repository/proxy. Such a fingerprint can
+ * be used to detect changes in the authentication data across JVM restarts without exposing sensitive information.
+ */
+public final class AuthenticationDigest
+{
+
+    private final MessageDigest digest;
+
+    private final RepositorySystemSession session;
+
+    private final RemoteRepository repository;
+
+    private final Proxy proxy;
+
+    /**
+     * Gets the fingerprint for the authentication of the specified repository.
+     * 
+     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
+     * @param repository The repository whose authentication is to be fingerprinted, must not be {@code null}.
+     * @return The fingerprint of the repository authentication or an empty string if no authentication is configured,
+     *         never {@code null}.
+     */
+    public static String forRepository( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String digest = "";
+        Authentication auth = repository.getAuthentication();
+        if ( auth != null )
+        {
+            AuthenticationDigest authDigest = new AuthenticationDigest( session, repository, null );
+            auth.digest( authDigest );
+            digest = authDigest.digest();
+        }
+        return digest;
+    }
+
+    /**
+     * Gets the fingerprint for the authentication of the specified repository's proxy.
+     * 
+     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
+     * @param repository The repository whose proxy authentication is to be fingerprinted, must not be {@code null}.
+     * @return The fingerprint of the proxy authentication or an empty string if no proxy is present or if no proxy
+     *         authentication is configured, never {@code null}.
+     */
+    public static String forProxy( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String digest = "";
+        Proxy proxy = repository.getProxy();
+        if ( proxy != null )
+        {
+            Authentication auth = proxy.getAuthentication();
+            if ( auth != null )
+            {
+                AuthenticationDigest authDigest = new AuthenticationDigest( session, repository, proxy );
+                auth.digest( authDigest );
+                digest = authDigest.digest();
+            }
+        }
+        return digest;
+    }
+
+    private AuthenticationDigest( RepositorySystemSession session, RemoteRepository repository, Proxy proxy )
+    {
+        this.session = session;
+        this.repository = repository;
+        this.proxy = proxy;
+        digest = newDigest();
+    }
+
+    private static MessageDigest newDigest()
+    {
+        try
+        {
+            return MessageDigest.getInstance( "SHA-1" );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            try
+            {
+                return MessageDigest.getInstance( "MD5" );
+            }
+            catch ( NoSuchAlgorithmException ne )
+            {
+                throw new IllegalStateException( ne );
+            }
+        }
+    }
+
+    /**
+     * Gets the repository system session during which the authentication fingerprint is calculated.
+     * 
+     * @return The repository system session, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
+     * this authentication digest does not apply to the repository's host but rather the proxy.
+     * 
+     * @return The repository to be contacted, never {@code null}.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Gets the proxy (if any) to be authenticated with.
+     * 
+     * @return The proxy or {@code null} if authenticating directly with the repository's host.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Updates the digest with the specified strings.
+     * 
+     * @param strings The strings to update the digest with, may be {@code null} or contain {@code null} elements.
+     */
+    public void update( String... strings )
+    {
+        if ( strings != null )
+        {
+            for ( String string : strings )
+            {
+                if ( string != null )
+                {
+                    digest.update( string.getBytes( StandardCharsets.UTF_8 ) );
+                }
+            }
+        }
+    }
+
+    /**
+     * Updates the digest with the specified characters.
+     * 
+     * @param chars The characters to update the digest with, may be {@code null}.
+     */
+    public void update( char... chars )
+    {
+        if ( chars != null )
+        {
+            for ( char c : chars )
+            {
+                digest.update( (byte) ( c >> 8 ) );
+                digest.update( (byte) ( c & 0xFF ) );
+            }
+        }
+    }
+
+    /**
+     * Updates the digest with the specified bytes.
+     * 
+     * @param bytes The bytes to update the digest with, may be {@code null}.
+     */
+    public void update( byte... bytes )
+    {
+        if ( bytes != null )
+        {
+            digest.update( bytes );
+        }
+    }
+
+    private String digest()
+    {
+        byte[] bytes = digest.digest();
+        StringBuilder buffer = new StringBuilder( bytes.length * 2 );
+        for ( byte aByte : bytes )
+        {
+            int b = aByte & 0xFF;
+            if ( b < 0x10 )
+            {
+                buffer.append( '0' );
+            }
+            buffer.append( Integer.toHexString( b ) );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationSelector.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationSelector.java
new file mode 100644
index 0000000..0637d1c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/AuthenticationSelector.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * Selects authentication for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getAuthenticationSelector()
+ */
+public interface AuthenticationSelector
+{
+
+    /**
+     * Selects authentication for the specified remote repository.
+     * 
+     * @param repository The repository for which to select authentication, must not be {@code null}.
+     * @return The selected authentication or {@code null} if none.
+     */
+    Authentication getAuthentication( RemoteRepository repository );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRegistration.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRegistration.java
new file mode 100644
index 0000000..1065779
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRegistration.java
@@ -0,0 +1,149 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A request to register an artifact within the local repository. Certain local repository implementations can refuse to
+ * serve physically present artifacts if those haven't been previously registered to them.
+ * 
+ * @see LocalRepositoryManager#add(RepositorySystemSession, LocalArtifactRegistration)
+ */
+public final class LocalArtifactRegistration
+{
+
+    private Artifact artifact;
+
+    private RemoteRepository repository;
+
+    private Collection<String> contexts = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized registration.
+     */
+    public LocalArtifactRegistration()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a registration request for the specified (locally installed) artifact.
+     * 
+     * @param artifact The artifact to register, may be {@code null}.
+     */
+    public LocalArtifactRegistration( Artifact artifact )
+    {
+        setArtifact( artifact );
+    }
+
+    /**
+     * Creates a registration request for the specified artifact.
+     * 
+     * @param artifact The artifact to register, may be {@code null}.
+     * @param repository The remote repository from which the artifact was resolved or {@code null} if the artifact was
+     *            locally installed.
+     * @param contexts The resolution contexts, may be {@code null}.
+     */
+    public LocalArtifactRegistration( Artifact artifact, RemoteRepository repository, Collection<String> contexts )
+    {
+        setArtifact( artifact );
+        setRepository( repository );
+        setContexts( contexts );
+    }
+
+    /**
+     * Gets the artifact to register.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to register.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the artifact was resolved.
+     * 
+     * @return The remote repository or {@code null} if the artifact was locally installed.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the artifact was resolved.
+     * 
+     * @param repository The remote repository or {@code null} if the artifact was locally installed.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the resolution contexts in which the artifact is available.
+     * 
+     * @return The resolution contexts in which the artifact is available, never {@code null}.
+     */
+    public Collection<String> getContexts()
+    {
+        return contexts;
+    }
+
+    /**
+     * Sets the resolution contexts in which the artifact is available.
+     * 
+     * @param contexts The resolution contexts, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalArtifactRegistration setContexts( Collection<String> contexts )
+    {
+        if ( contexts != null )
+        {
+            this.contexts = contexts;
+        }
+        else
+        {
+            this.contexts = Collections.emptyList();
+        }
+        return this;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRequest.java
new file mode 100644
index 0000000..8f6eabf
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactRequest.java
@@ -0,0 +1,145 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A query to the local repository for the existence of an artifact.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalArtifactRequest)
+ */
+public final class LocalArtifactRequest
+{
+
+    private Artifact artifact;
+
+    private String context = "";
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized query.
+     */
+    public LocalArtifactRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a query with the specified properties.
+     * 
+     * @param artifact The artifact to query for, may be {@code null}.
+     * @param repositories The remote repositories that should be considered as potential sources for the artifact, may
+     *            be {@code null} or empty to only consider locally installed artifacts.
+     * @param context The resolution context for the artifact, may be {@code null}.
+     */
+    public LocalArtifactRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setContext( context );
+    }
+
+    /**
+     * Gets the artifact to query for.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to query for.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the resolution context.
+     * 
+     * @return The resolution context, never {@code null}.
+     */
+    public String getContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the resolution context.
+     * 
+     * @param context The resolution context, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories to consider as sources of the artifact.
+     * 
+     * @return The remote repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories to consider as sources of the artifact.
+     * 
+     * @param repositories The remote repositories, may be {@code null} or empty to only consider locally installed
+     *            artifacts.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalArtifactRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories != null )
+        {
+            this.repositories = repositories;
+        }
+        else
+        {
+            this.repositories = Collections.emptyList();
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " @ " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java
new file mode 100644
index 0000000..34dbe0a
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java
@@ -0,0 +1,144 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A result from the local repository about the existence of an artifact.
+ *
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalArtifactRequest)
+ */
+public final class LocalArtifactResult
+{
+
+    private final LocalArtifactRequest request;
+
+    private File file;
+
+    private boolean available;
+
+    private RemoteRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The local artifact request, must not be {@code null}.
+     */
+    public LocalArtifactResult( LocalArtifactRequest request )
+    {
+        this.request = requireNonNull( request, "local artifact request cannot be null" );
+    }
+
+    /**
+     * Gets the request corresponding to this result.
+     *
+     * @return The corresponding request, never {@code null}.
+     */
+    public LocalArtifactRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the file to the requested artifact. Note that this file must not be used unless {@link #isAvailable()}
+     * returns {@code true}. An artifact file can be found but considered unavailable if the artifact was cached from a
+     * remote repository that is not part of the list of remote repositories used for the query.
+     * 
+     * @return The file to the requested artifact or {@code null} if the artifact does not exist locally.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the file to requested artifact.
+     * 
+     * @param file The artifact file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * Indicates whether the requested artifact is available for use. As a minimum, the file needs to be physically
+     * existent in the local repository to be available. Additionally, a local repository manager can consider the list
+     * of supplied remote repositories to determine whether the artifact is logically available and mark an artifact
+     * unavailable (despite its physical existence) if it is not known to be hosted by any of the provided repositories.
+     * 
+     * @return {@code true} if the artifact is available, {@code false} otherwise.
+     * @see LocalArtifactRequest#getRepositories()
+     */
+    public boolean isAvailable()
+    {
+        return available;
+    }
+
+    /**
+     * Sets whether the artifact is available.
+     * 
+     * @param available {@code true} if the artifact is available, {@code false} otherwise.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setAvailable( boolean available )
+    {
+        this.available = available;
+        return this;
+    }
+
+    /**
+     * Gets the (first) remote repository from which the artifact was cached (if any).
+     * 
+     * @return The remote repository from which the artifact was originally retrieved or {@code null} if unknown or if
+     *         the artifact has been locally installed.
+     * @see LocalArtifactRequest#getRepositories()
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the (first) remote repository from which the artifact was cached.
+     * 
+     * @param repository The remote repository from which the artifact was originally retrieved, may be {@code null} if
+     *            unknown or if the artifact has been locally installed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalArtifactResult setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getFile() + " (" + ( isAvailable() ? "available" : "unavailable" ) + ")";
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRegistration.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRegistration.java
new file mode 100644
index 0000000..dd0d587
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRegistration.java
@@ -0,0 +1,148 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A request to register metadata within the local repository.
+ * 
+ * @see LocalRepositoryManager#add(RepositorySystemSession, LocalMetadataRegistration)
+ */
+public final class LocalMetadataRegistration
+{
+
+    private Metadata metadata;
+
+    private RemoteRepository repository;
+
+    private Collection<String> contexts = Collections.emptyList();
+
+    /**
+     * Creates an uninitialized registration.
+     */
+    public LocalMetadataRegistration()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a registration request for the specified metadata accompanying a locally installed artifact.
+     * 
+     * @param metadata The metadata to register, may be {@code null}.
+     */
+    public LocalMetadataRegistration( Metadata metadata )
+    {
+        setMetadata( metadata );
+    }
+
+    /**
+     * Creates a registration request for the specified metadata.
+     * 
+     * @param metadata The metadata to register, may be {@code null}.
+     * @param repository The remote repository from which the metadata was resolved or {@code null} if the metadata
+     *            accompanies a locally installed artifact.
+     * @param contexts The resolution contexts, may be {@code null}.
+     */
+    public LocalMetadataRegistration( Metadata metadata, RemoteRepository repository, Collection<String> contexts )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setContexts( contexts );
+    }
+
+    /**
+     * Gets the metadata to register.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to register.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the metadata was resolved.
+     * 
+     * @return The remote repository or {@code null} if the metadata was locally installed.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the metadata was resolved.
+     * 
+     * @param repository The remote repository or {@code null} if the metadata accompanies a locally installed artifact.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the resolution contexts in which the metadata is available.
+     * 
+     * @return The resolution contexts in which the metadata is available, never {@code null}.
+     */
+    public Collection<String> getContexts()
+    {
+        return contexts;
+    }
+
+    /**
+     * Sets the resolution contexts in which the metadata is available.
+     * 
+     * @param contexts The resolution contexts, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public LocalMetadataRegistration setContexts( Collection<String> contexts )
+    {
+        if ( contexts != null )
+        {
+            this.contexts = contexts;
+        }
+        else
+        {
+            this.contexts = Collections.emptyList();
+        }
+        return this;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRequest.java
new file mode 100644
index 0000000..4c8f270
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataRequest.java
@@ -0,0 +1,133 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A query to the local repository for the existence of metadata.
+ * 
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalMetadataRequest)
+ */
+public final class LocalMetadataRequest
+{
+
+    private Metadata metadata;
+
+    private String context = "";
+
+    private RemoteRepository repository = null;
+
+    /**
+     * Creates an uninitialized query.
+     */
+    public LocalMetadataRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a query with the specified properties.
+     * 
+     * @param metadata The metadata to query for, may be {@code null}.
+     * @param repository The source remote repository for the metadata, may be {@code null} for local metadata.
+     * @param context The resolution context for the metadata, may be {@code null}.
+     */
+    public LocalMetadataRequest( Metadata metadata, RemoteRepository repository, String context )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setContext( context );
+    }
+
+    /**
+     * Gets the metadata to query for.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to query for.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalMetadataRequest setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the resolution context.
+     * 
+     * @return The resolution context, never {@code null}.
+     */
+    public String getContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the resolution context.
+     * 
+     * @param context The resolution context, may be {@code null}.
+     * @return This query for chaining, never {@code null}.
+     */
+    public LocalMetadataRequest setContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the remote repository to use as source of the metadata.
+     * 
+     * @return The remote repositories, may be {@code null} for local metadata.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository to use as sources of the metadata.
+     * 
+     * @param repository The remote repository, may be {@code null}.
+     * @return This query for chaining, may be {@code null} for local metadata.
+     */
+    public LocalMetadataRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " @ " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java
new file mode 100644
index 0000000..12f3a35
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java
@@ -0,0 +1,111 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A result from the local repository about the existence of metadata.
+ *
+ * @see LocalRepositoryManager#find(RepositorySystemSession, LocalMetadataRequest)
+ */
+public final class LocalMetadataResult
+{
+
+    private final LocalMetadataRequest request;
+
+    private File file;
+
+    private boolean stale;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The local metadata request, must not be {@code null}.
+     */
+    public LocalMetadataResult( LocalMetadataRequest request )
+    {
+        this.request = requireNonNull( request, "local metadata request cannot be null" );
+    }
+
+    /**
+     * Gets the request corresponding to this result.
+     *
+     * @return The corresponding request, never {@code null}.
+     */
+    public LocalMetadataRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the file to the requested metadata if the metadata is available in the local repository.
+     * 
+     * @return The file to the requested metadata or {@code null}.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the file to requested metadata.
+     * 
+     * @param file The metadata file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalMetadataResult setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * This value indicates whether the metadata is stale and should be updated.
+     * 
+     * @return {@code true} if the metadata is stale and should be updated, {@code false} otherwise.
+     */
+    public boolean isStale()
+    {
+        return stale;
+    }
+
+    /**
+     * Sets whether the metadata is stale.
+     * 
+     * @param stale {@code true} if the metadata is stale and should be updated, {@code false} otherwise.
+     * @return This result for chaining, never {@code null}.
+     */
+    public LocalMetadataResult setStale( boolean stale )
+    {
+        this.stale = stale;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return request.toString() + "(" + getFile() + ")";
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
new file mode 100644
index 0000000..32dce73
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
@@ -0,0 +1,132 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+/**
+ * A repository on the local file system used to cache contents of remote repositories and to store locally installed
+ * artifacts. Note that this class merely describes such a repository, actual access to the contained artifacts is
+ * handled by a {@link LocalRepositoryManager} which is usually determined from the {@link #getContentType() type} of
+ * the repository.
+ */
+public final class LocalRepository
+    implements ArtifactRepository
+{
+
+    private final File basedir;
+
+    private final String type;
+
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     */
+    public LocalRepository( String basedir )
+    {
+        this( ( basedir != null ) ? new File( basedir ) : null, "" );
+    }
+
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     */
+    public LocalRepository( File basedir )
+    {
+        this( basedir, "" );
+    }
+
+    /**
+     * Creates a new local repository with the specified properties.
+     * 
+     * @param basedir The base directory of the repository, may be {@code null}.
+     * @param type The type of the repository, may be {@code null}.
+     */
+    public LocalRepository( File basedir, String type )
+    {
+        this.basedir = basedir;
+        this.type = ( type != null ) ? type : "";
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    public String getId()
+    {
+        return "local";
+    }
+
+    /**
+     * Gets the base directory of the repository.
+     * 
+     * @return The base directory or {@code null} if none.
+     */
+    public File getBasedir()
+    {
+        return basedir;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getBasedir() + " (" + getContentType() + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        LocalRepository that = (LocalRepository) obj;
+
+        return eq( basedir, that.basedir ) && eq( type, that.type );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( basedir );
+        hash = hash * 31 + hash( type );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepositoryManager.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepositoryManager.java
new file mode 100644
index 0000000..649707c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepositoryManager.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * Manages access to a local repository.
+ * 
+ * @see RepositorySystemSession#getLocalRepositoryManager()
+ * @see org.eclipse.aether.RepositorySystem#newLocalRepositoryManager(RepositorySystemSession, LocalRepository)
+ */
+public interface LocalRepositoryManager
+{
+
+    /**
+     * Gets the description of the local repository being managed.
+     * 
+     * @return The description of the local repository, never {@code null}.
+     */
+    LocalRepository getRepository();
+
+    /**
+     * Gets the relative path for a locally installed artifact. Note that the artifact need not actually exist yet at
+     * the returned location, the path merely indicates where the artifact would eventually be stored. The path uses the
+     * forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param artifact The artifact for which to determine the path, must not be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForLocalArtifact( Artifact artifact );
+
+    /**
+     * Gets the relative path for an artifact cached from a remote repository. Note that the artifact need not actually
+     * exist yet at the returned location, the path merely indicates where the artifact would eventually be stored. The
+     * path uses the forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param artifact The artifact for which to determine the path, must not be {@code null}.
+     * @param repository The source repository of the artifact, must not be {@code null}.
+     * @param context The resolution context in which the artifact is being requested, may be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context );
+
+    /**
+     * Gets the relative path for locally installed metadata. Note that the metadata need not actually exist yet at the
+     * returned location, the path merely indicates where the metadata would eventually be stored. The path uses the
+     * forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param metadata The metadata for which to determine the path, must not be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForLocalMetadata( Metadata metadata );
+
+    /**
+     * Gets the relative path for metadata cached from a remote repository. Note that the metadata need not actually
+     * exist yet at the returned location, the path merely indicates where the metadata would eventually be stored. The
+     * path uses the forward slash as directory separator regardless of the underlying file system.
+     * 
+     * @param metadata The metadata for which to determine the path, must not be {@code null}.
+     * @param repository The source repository of the metadata, must not be {@code null}.
+     * @param context The resolution context in which the metadata is being requested, may be {@code null}.
+     * @return The path, relative to the local repository's base directory.
+     */
+    String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context );
+
+    /**
+     * Queries for the existence of an artifact in the local repository. The request could be satisfied by a locally
+     * installed artifact or a previously downloaded artifact.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param request The artifact request, must not be {@code null}.
+     * @return The result of the request, never {@code null}.
+     */
+    LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request );
+
+    /**
+     * Registers an installed or resolved artifact with the local repository. Note that artifact registration is merely
+     * concerned about updating the local repository's internal state, not about actually installing the artifact or its
+     * accompanying metadata.
+     * 
+     * @param session The repository system session during which the registration is made, must not be {@code null}.
+     * @param request The registration request, must not be {@code null}.
+     */
+    void add( RepositorySystemSession session, LocalArtifactRegistration request );
+
+    /**
+     * Queries for the existence of metadata in the local repository. The request could be satisfied by locally
+     * installed or previously downloaded metadata.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param request The metadata request, must not be {@code null}.
+     * @return The result of the request, never {@code null}.
+     */
+    LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request );
+
+    /**
+     * Registers installed or resolved metadata with the local repository. Note that metadata registration is merely
+     * concerned about updating the local repository's internal state, not about actually installing the metadata.
+     * However, this method MUST be called after the actual install to give the repository manager the opportunity to
+     * inspect the added metadata.
+     * 
+     * @param session The repository system session during which the registration is made, must not be {@code null}.
+     * @param request The registration request, must not be {@code null}.
+     */
+    void add( RepositorySystemSession session, LocalMetadataRegistration request );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/MirrorSelector.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/MirrorSelector.java
new file mode 100644
index 0000000..d50262c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/MirrorSelector.java
@@ -0,0 +1,39 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * Selects a mirror for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getMirrorSelector()
+ */
+public interface MirrorSelector
+{
+
+    /**
+     * Selects a mirror for the specified repository.
+     * 
+     * @param repository The repository to select a mirror for, must not be {@code null}.
+     * @return The selected mirror or {@code null} if none.
+     * @see RemoteRepository#getMirroredRepositories()
+     */
+    RemoteRepository getMirror( RemoteRepository repository );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
new file mode 100644
index 0000000..c804821
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
@@ -0,0 +1,102 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unsupported local repository type.
+ */
+public class NoLocalRepositoryManagerException
+    extends RepositoryException
+{
+
+    private final transient LocalRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The local repository for which no support is available, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoLocalRepositoryManagerException( LocalRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( LocalRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "No manager available for local repository (" + repository.getBasedir().getAbsolutePath()
+                + ") of type " + repository.getContentType();
+        }
+        else
+        {
+            return "No manager available for local repository";
+        }
+    }
+
+    /**
+     * Gets the local repository whose content type is not supported.
+     * 
+     * @return The unsupported local repository or {@code null} if unknown.
+     */
+    public LocalRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Proxy.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Proxy.java
new file mode 100644
index 0000000..8e8cba1
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/Proxy.java
@@ -0,0 +1,158 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * A proxy to use for connections to a repository.
+ */
+public final class Proxy
+{
+
+    /**
+     * Type denoting a proxy for HTTP transfers.
+     */
+    public static final String TYPE_HTTP = "http";
+
+    /**
+     * Type denoting a proxy for HTTPS transfers.
+     */
+    public static final String TYPE_HTTPS = "https";
+
+    private final String type;
+
+    private final String host;
+
+    private final int port;
+
+    private final Authentication auth;
+
+    /**
+     * Creates a new proxy with the specified properties and no authentication.
+     * 
+     * @param type The type of the proxy, e.g. "http", may be {@code null}.
+     * @param host The host of the proxy, may be {@code null}.
+     * @param port The port of the proxy.
+     */
+    public Proxy( String type, String host, int port )
+    {
+        this( type, host, port, null );
+    }
+
+    /**
+     * Creates a new proxy with the specified properties.
+     * 
+     * @param type The type of the proxy, e.g. "http", may be {@code null}.
+     * @param host The host of the proxy, may be {@code null}.
+     * @param port The port of the proxy.
+     * @param auth The authentication to use for the proxy connection, may be {@code null}.
+     */
+    public Proxy( String type, String host, int port, Authentication auth )
+    {
+        this.type = ( type != null ) ? type : "";
+        this.host = ( host != null ) ? host : "";
+        this.port = port;
+        this.auth = auth;
+    }
+
+    /**
+     * Gets the type of this proxy.
+     * 
+     * @return The type of this proxy, never {@code null}.
+     */
+    public String getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the host for this proxy.
+     * 
+     * @return The host for this proxy, never {@code null}.
+     */
+    public String getHost()
+    {
+        return host;
+    }
+
+    /**
+     * Gets the port number for this proxy.
+     * 
+     * @return The port number for this proxy.
+     */
+    public int getPort()
+    {
+        return port;
+    }
+
+    /**
+     * Gets the authentication to use for the proxy connection.
+     * 
+     * @return The authentication to use or {@code null} if none.
+     */
+    public Authentication getAuthentication()
+    {
+        return auth;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getHost() + ':' + getPort();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        Proxy that = (Proxy) obj;
+
+        return eq( type, that.type ) && eq( host, that.host ) && port == that.port && eq( auth, that.auth );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( host );
+        hash = hash * 31 + hash( type );
+        hash = hash * 31 + port;
+        hash = hash * 31 + hash( auth );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ProxySelector.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ProxySelector.java
new file mode 100644
index 0000000..29b9e4e
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/ProxySelector.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * Selects a proxy for a given remote repository.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getProxySelector()
+ */
+public interface ProxySelector
+{
+
+    /**
+     * Selects a proxy for the specified remote repository.
+     * 
+     * @param repository The repository for which to select a proxy, must not be {@code null}.
+     * @return The selected proxy or {@code null} if none.
+     */
+    Proxy getProxy( RemoteRepository repository );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RemoteRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RemoteRepository.java
new file mode 100644
index 0000000..f854051
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RemoteRepository.java
@@ -0,0 +1,579 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A repository on a remote server.
+ */
+public final class RemoteRepository
+    implements ArtifactRepository
+{
+
+    private static final Pattern URL_PATTERN =
+        Pattern.compile( "([^:/]+(:[^:/]{2,}+(?=://))?):(//([^@/]*@)?([^/:]+))?.*" );
+
+    private final String id;
+
+    private final String type;
+
+    private final String url;
+
+    private final String host;
+
+    private final String protocol;
+
+    private final RepositoryPolicy releasePolicy;
+
+    private final RepositoryPolicy snapshotPolicy;
+
+    private final Proxy proxy;
+
+    private final Authentication authentication;
+
+    private final List<RemoteRepository> mirroredRepositories;
+
+    private final boolean repositoryManager;
+
+    RemoteRepository( Builder builder )
+    {
+        if ( builder.prototype != null )
+        {
+            id = ( builder.delta & Builder.ID ) != 0 ? builder.id : builder.prototype.id;
+            type = ( builder.delta & Builder.TYPE ) != 0 ? builder.type : builder.prototype.type;
+            url = ( builder.delta & Builder.URL ) != 0 ? builder.url : builder.prototype.url;
+            releasePolicy =
+                ( builder.delta & Builder.RELEASES ) != 0 ? builder.releasePolicy : builder.prototype.releasePolicy;
+            snapshotPolicy =
+                ( builder.delta & Builder.SNAPSHOTS ) != 0 ? builder.snapshotPolicy : builder.prototype.snapshotPolicy;
+            proxy = ( builder.delta & Builder.PROXY ) != 0 ? builder.proxy : builder.prototype.proxy;
+            authentication =
+                ( builder.delta & Builder.AUTH ) != 0 ? builder.authentication : builder.prototype.authentication;
+            repositoryManager =
+                ( builder.delta & Builder.REPOMAN ) != 0 ? builder.repositoryManager
+                                : builder.prototype.repositoryManager;
+            mirroredRepositories =
+                ( builder.delta & Builder.MIRRORED ) != 0 ? copy( builder.mirroredRepositories )
+                                : builder.prototype.mirroredRepositories;
+        }
+        else
+        {
+            id = builder.id;
+            type = builder.type;
+            url = builder.url;
+            releasePolicy = builder.releasePolicy;
+            snapshotPolicy = builder.snapshotPolicy;
+            proxy = builder.proxy;
+            authentication = builder.authentication;
+            repositoryManager = builder.repositoryManager;
+            mirroredRepositories = copy( builder.mirroredRepositories );
+        }
+
+        Matcher m = URL_PATTERN.matcher( url );
+        if ( m.matches() )
+        {
+            protocol = m.group( 1 );
+            String host = m.group( 5 );
+            this.host = ( host != null ) ? host : "";
+        }
+        else
+        {
+            protocol = host = "";
+        }
+    }
+
+    private static List<RemoteRepository> copy( List<RemoteRepository> repos )
+    {
+        if ( repos == null || repos.isEmpty() )
+        {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList( Arrays.asList( repos.toArray( new RemoteRepository[repos.size()] ) ) );
+    }
+
+    public String getId()
+    {
+        return id;
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the (base) URL of this repository.
+     * 
+     * @return The (base) URL of this repository, never {@code null}.
+     */
+    public String getUrl()
+    {
+        return url;
+    }
+
+    /**
+     * Gets the protocol part from the repository's URL, for example {@code file} or {@code http}. As suggested by RFC
+     * 2396, section 3.1 "Scheme Component", the protocol name should be treated case-insensitively.
+     * 
+     * @return The protocol or an empty string if none, never {@code null}.
+     */
+    public String getProtocol()
+    {
+        return protocol;
+    }
+
+    /**
+     * Gets the host part from the repository's URL.
+     * 
+     * @return The host or an empty string if none, never {@code null}.
+     */
+    public String getHost()
+    {
+        return host;
+    }
+
+    /**
+     * Gets the policy to apply for snapshot/release artifacts.
+     * 
+     * @param snapshot {@code true} to retrieve the snapshot policy, {@code false} to retrieve the release policy.
+     * @return The requested repository policy, never {@code null}.
+     */
+    public RepositoryPolicy getPolicy( boolean snapshot )
+    {
+        return snapshot ? snapshotPolicy : releasePolicy;
+    }
+
+    /**
+     * Gets the proxy that has been selected for this repository.
+     * 
+     * @return The selected proxy or {@code null} if none.
+     */
+    public Proxy getProxy()
+    {
+        return proxy;
+    }
+
+    /**
+     * Gets the authentication that has been selected for this repository.
+     * 
+     * @return The selected authentication or {@code null} if none.
+     */
+    public Authentication getAuthentication()
+    {
+        return authentication;
+    }
+
+    /**
+     * Gets the repositories that this repository serves as a mirror for.
+     * 
+     * @return The (read-only) repositories being mirrored by this repository, never {@code null}.
+     */
+    public List<RemoteRepository> getMirroredRepositories()
+    {
+        return mirroredRepositories;
+    }
+
+    /**
+     * Indicates whether this repository refers to a repository manager or not.
+     * 
+     * @return {@code true} if this repository is a repository manager, {@code false} otherwise.
+     */
+    public boolean isRepositoryManager()
+    {
+        return repositoryManager;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( getId() );
+        buffer.append( " (" ).append( getUrl() );
+        buffer.append( ", " ).append( getContentType() );
+        boolean r = getPolicy( false ).isEnabled(), s = getPolicy( true ).isEnabled();
+        if ( r && s )
+        {
+            buffer.append( ", releases+snapshots" );
+        }
+        else if ( r )
+        {
+            buffer.append( ", releases" );
+        }
+        else if ( s )
+        {
+            buffer.append( ", snapshots" );
+        }
+        else
+        {
+            buffer.append( ", disabled" );
+        }
+        if ( isRepositoryManager() )
+        {
+            buffer.append( ", managed" );
+        }
+        buffer.append( ")" );
+        return buffer.toString();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        RemoteRepository that = (RemoteRepository) obj;
+
+        return eq( url, that.url ) && eq( type, that.type ) && eq( id, that.id )
+            && eq( releasePolicy, that.releasePolicy ) && eq( snapshotPolicy, that.snapshotPolicy )
+            && eq( proxy, that.proxy ) && eq( authentication, that.authentication )
+            && eq( mirroredRepositories, that.mirroredRepositories ) && repositoryManager == that.repositoryManager;
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( url );
+        hash = hash * 31 + hash( type );
+        hash = hash * 31 + hash( id );
+        hash = hash * 31 + hash( releasePolicy );
+        hash = hash * 31 + hash( snapshotPolicy );
+        hash = hash * 31 + hash( proxy );
+        hash = hash * 31 + hash( authentication );
+        hash = hash * 31 + hash( mirroredRepositories );
+        hash = hash * 31 + ( repositoryManager ? 1 : 0 );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+    /**
+     * A builder to create remote repositories.
+     */
+    public static final class Builder
+    {
+
+        private static final RepositoryPolicy DEFAULT_POLICY = new RepositoryPolicy();
+
+        static final int ID = 0x0001, TYPE = 0x0002, URL = 0x0004, RELEASES = 0x0008, SNAPSHOTS = 0x0010,
+                        PROXY = 0x0020, AUTH = 0x0040, MIRRORED = 0x0080, REPOMAN = 0x0100;
+
+        int delta;
+
+        RemoteRepository prototype;
+
+        String id;
+
+        String type;
+
+        String url;
+
+        RepositoryPolicy releasePolicy = DEFAULT_POLICY;
+
+        RepositoryPolicy snapshotPolicy = DEFAULT_POLICY;
+
+        Proxy proxy;
+
+        Authentication authentication;
+
+        List<RemoteRepository> mirroredRepositories;
+
+        boolean repositoryManager;
+
+        /**
+         * Creates a new repository builder.
+         * 
+         * @param id The identifier of the repository, may be {@code null}.
+         * @param type The type of the repository, may be {@code null}.
+         * @param url The (base) URL of the repository, may be {@code null}.
+         */
+        public Builder( String id, String type, String url )
+        {
+            this.id = ( id != null ) ? id : "";
+            this.type = ( type != null ) ? type : "";
+            this.url = ( url != null ) ? url : "";
+        }
+
+        /**
+         * Creates a new repository builder which uses the specified remote repository as a prototype for the new one.
+         * All properties which have not been set on the builder will be copied from the prototype when building the
+         * repository.
+         *
+         * @param prototype The remote repository to use as prototype, must not be {@code null}.
+         */
+        public Builder( RemoteRepository prototype )
+        {
+            this.prototype = requireNonNull( prototype, "remote repository prototype cannot be null" );
+        }
+
+        /**
+         * Builds a new remote repository from the current values of this builder. The state of the builder itself
+         * remains unchanged.
+         *
+         * @return The remote repository, never {@code null}.
+         */
+        public RemoteRepository build()
+        {
+            if ( prototype != null && delta == 0 )
+            {
+                return prototype;
+            }
+            return new RemoteRepository( this );
+        }
+
+        private <T> void delta( int flag, T builder, T prototype )
+        {
+            boolean equal = ( builder != null ) ? builder.equals( prototype ) : prototype == null;
+            if ( equal )
+            {
+                delta &= ~flag;
+            }
+            else
+            {
+                delta |= flag;
+            }
+        }
+
+        /**
+         * Sets the identifier of the repository.
+         * 
+         * @param id The identifier of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setId( String id )
+        {
+            this.id = ( id != null ) ? id : "";
+            if ( prototype != null )
+            {
+                delta( ID, this.id, prototype.getId() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the type of the repository, e.g. "default".
+         * 
+         * @param type The type of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setContentType( String type )
+        {
+            this.type = ( type != null ) ? type : "";
+            if ( prototype != null )
+            {
+                delta( TYPE, this.type, prototype.getContentType() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the (base) URL of the repository.
+         * 
+         * @param url The URL of the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setUrl( String url )
+        {
+            this.url = ( url != null ) ? url : "";
+            if ( prototype != null )
+            {
+                delta( URL, this.url, prototype.getUrl() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for snapshot and release artifacts.
+         * 
+         * @param policy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setPolicy( RepositoryPolicy policy )
+        {
+            this.releasePolicy = this.snapshotPolicy = ( policy != null ) ? policy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( RELEASES, this.releasePolicy, prototype.getPolicy( false ) );
+                delta( SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy( true ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for release artifacts.
+         * 
+         * @param releasePolicy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setReleasePolicy( RepositoryPolicy releasePolicy )
+        {
+            this.releasePolicy = ( releasePolicy != null ) ? releasePolicy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( RELEASES, this.releasePolicy, prototype.getPolicy( false ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the policy to apply for snapshot artifacts.
+         * 
+         * @param snapshotPolicy The repository policy to set, may be {@code null} to use a default policy.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setSnapshotPolicy( RepositoryPolicy snapshotPolicy )
+        {
+            this.snapshotPolicy = ( snapshotPolicy != null ) ? snapshotPolicy : DEFAULT_POLICY;
+            if ( prototype != null )
+            {
+                delta( SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy( true ) );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the proxy to use in order to access the repository.
+         * 
+         * @param proxy The proxy to use, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setProxy( Proxy proxy )
+        {
+            this.proxy = proxy;
+            if ( prototype != null )
+            {
+                delta( PROXY, this.proxy, prototype.getProxy() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the authentication to use in order to access the repository.
+         * 
+         * @param authentication The authentication to use, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setAuthentication( Authentication authentication )
+        {
+            this.authentication = authentication;
+            if ( prototype != null )
+            {
+                delta( AUTH, this.authentication, prototype.getAuthentication() );
+            }
+            return this;
+        }
+
+        /**
+         * Sets the repositories being mirrored by the repository.
+         * 
+         * @param mirroredRepositories The repositories being mirrored by the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setMirroredRepositories( List<RemoteRepository> mirroredRepositories )
+        {
+            if ( this.mirroredRepositories == null )
+            {
+                this.mirroredRepositories = new ArrayList<RemoteRepository>();
+            }
+            else
+            {
+                this.mirroredRepositories.clear();
+            }
+            if ( mirroredRepositories != null )
+            {
+                this.mirroredRepositories.addAll( mirroredRepositories );
+            }
+            if ( prototype != null )
+            {
+                delta( MIRRORED, this.mirroredRepositories, prototype.getMirroredRepositories() );
+            }
+            return this;
+        }
+
+        /**
+         * Adds the specified repository to the list of repositories being mirrored by the repository. If this builder
+         * was {@link #RemoteRepository.Builder(RemoteRepository) constructed from a prototype}, the given repository
+         * will be added to the list of mirrored repositories from the prototype.
+         * 
+         * @param mirroredRepository The repository being mirrored by the repository, may be {@code null}.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder addMirroredRepository( RemoteRepository mirroredRepository )
+        {
+            if ( mirroredRepository != null )
+            {
+                if ( this.mirroredRepositories == null )
+                {
+                    this.mirroredRepositories = new ArrayList<RemoteRepository>();
+                    if ( prototype != null )
+                    {
+                        mirroredRepositories.addAll( prototype.getMirroredRepositories() );
+                    }
+                }
+                mirroredRepositories.add( mirroredRepository );
+                if ( prototype != null )
+                {
+                    delta |= MIRRORED;
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Marks the repository as a repository manager or not.
+         * 
+         * @param repositoryManager {@code true} if the repository points at a repository manager, {@code false} if the
+         *            repository is just serving static contents.
+         * @return This builder for chaining, never {@code null}.
+         */
+        public Builder setRepositoryManager( boolean repositoryManager )
+        {
+            this.repositoryManager = repositoryManager;
+            if ( prototype != null )
+            {
+                delta( REPOMAN, this.repositoryManager, prototype.isRepositoryManager() );
+            }
+            return this;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryPolicy.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryPolicy.java
new file mode 100644
index 0000000..18fb850
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryPolicy.java
@@ -0,0 +1,161 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+/**
+ * A policy controlling access to a repository.
+ */
+public final class RepositoryPolicy
+{
+
+    /**
+     * Never update locally cached data.
+     */
+    public static final String UPDATE_POLICY_NEVER = "never";
+
+    /**
+     * Always update locally cached data.
+     */
+    public static final String UPDATE_POLICY_ALWAYS = "always";
+
+    /**
+     * Update locally cached data once a day.
+     */
+    public static final String UPDATE_POLICY_DAILY = "daily";
+
+    /**
+     * Update locally cached data every X minutes as given by "interval:X".
+     */
+    public static final String UPDATE_POLICY_INTERVAL = "interval";
+
+    /**
+     * Verify checksums and fail the resolution if they do not match.
+     */
+    public static final String CHECKSUM_POLICY_FAIL = "fail";
+
+    /**
+     * Verify checksums and warn if they do not match.
+     */
+    public static final String CHECKSUM_POLICY_WARN = "warn";
+
+    /**
+     * Do not verify checksums.
+     */
+    public static final String CHECKSUM_POLICY_IGNORE = "ignore";
+
+    private final boolean enabled;
+
+    private final String updatePolicy;
+
+    private final String checksumPolicy;
+
+    /**
+     * Creates a new policy with checksum warnings and daily update checks.
+     */
+    public RepositoryPolicy()
+    {
+        this( true, UPDATE_POLICY_DAILY, CHECKSUM_POLICY_WARN );
+    }
+
+    /**
+     * Creates a new policy with the specified settings.
+     * 
+     * @param enabled A flag whether the associated repository should be accessed or not.
+     * @param updatePolicy The update interval after which locally cached data from the repository is considered stale
+     *            and should be refetched, may be {@code null}.
+     * @param checksumPolicy The way checksum verification should be handled, may be {@code null}.
+     */
+    public RepositoryPolicy( boolean enabled, String updatePolicy, String checksumPolicy )
+    {
+        this.enabled = enabled;
+        this.updatePolicy = ( updatePolicy != null ) ? updatePolicy : "";
+        this.checksumPolicy = ( checksumPolicy != null ) ? checksumPolicy : "";
+    }
+
+    /**
+     * Indicates whether the associated repository should be contacted or not.
+     * 
+     * @return {@code true} if the repository should be contacted, {@code false} otherwise.
+     */
+    public boolean isEnabled()
+    {
+        return enabled;
+    }
+
+    /**
+     * Gets the update policy for locally cached data from the repository.
+     * 
+     * @return The update policy, never {@code null}.
+     */
+    public String getUpdatePolicy()
+    {
+        return updatePolicy;
+    }
+
+    /**
+     * Gets the policy for checksum validation.
+     * 
+     * @return The checksum policy, never {@code null}.
+     */
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "enabled=" ).append( isEnabled() );
+        buffer.append( ", checksums=" ).append( getChecksumPolicy() );
+        buffer.append( ", updates=" ).append( getUpdatePolicy() );
+        return buffer.toString();
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        RepositoryPolicy that = (RepositoryPolicy) obj;
+
+        return enabled == that.enabled && updatePolicy.equals( that.updatePolicy )
+            && checksumPolicy.equals( that.checksumPolicy );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + ( enabled ? 1 : 0 );
+        hash = hash * 31 + updatePolicy.hashCode();
+        hash = hash * 31 + checksumPolicy.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java
new file mode 100644
index 0000000..d1140f3
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * Manages a repository backed by the IDE workspace, a build session or a similar ad-hoc collection of artifacts.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getWorkspaceReader()
+ */
+public interface WorkspaceReader
+{
+
+    /**
+     * Gets a description of the workspace repository.
+     * 
+     * @return The repository description, never {@code null}.
+     */
+    WorkspaceRepository getRepository();
+
+    /**
+     * Locates the specified artifact.
+     * 
+     * @param artifact The artifact to locate, must not be {@code null}.
+     * @return The path to the artifact or {@code null} if the artifact is not available.
+     */
+    File findArtifact( Artifact artifact );
+
+    /**
+     * Determines all available versions of the specified artifact.
+     * 
+     * @param artifact The artifact whose versions should be listed, must not be {@code null}.
+     * @return The available versions of the artifact, must not be {@code null}.
+     */
+    List<String> findVersions( Artifact artifact );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
new file mode 100644
index 0000000..38dc5c5
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
@@ -0,0 +1,122 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.UUID;
+
+/**
+ * A repository backed by an IDE workspace, the output of a build session or similar ad-hoc collection of artifacts. As
+ * far as the repository system is concerned, a workspace repository is read-only, i.e. can only be used for artifact
+ * resolution but not installation/deployment. Note that this class merely describes such a repository, actual access to
+ * the contained artifacts is handled by a {@link WorkspaceReader}.
+ */
+public final class WorkspaceRepository
+    implements ArtifactRepository
+{
+
+    private final String type;
+
+    private final Object key;
+
+    /**
+     * Creates a new workspace repository of type {@code "workspace"} and a random key.
+     */
+    public WorkspaceRepository()
+    {
+        this( "workspace" );
+    }
+
+    /**
+     * Creates a new workspace repository with the specified type and a random key.
+     * 
+     * @param type The type of the repository, may be {@code null}.
+     */
+    public WorkspaceRepository( String type )
+    {
+        this( type, null );
+    }
+
+    /**
+     * Creates a new workspace repository with the specified type and key. The key is used to distinguish one workspace
+     * from another and should be sensitive to the artifacts that are (potentially) available in the workspace.
+     * 
+     * @param type The type of the repository, may be {@code null}.
+     * @param key The (comparison) key for the repository, may be {@code null} to generate a unique random key.
+     */
+    public WorkspaceRepository( String type, Object key )
+    {
+        this.type = ( type != null ) ? type : "";
+        this.key = ( key != null ) ? key : UUID.randomUUID().toString().replace( "-", "" );
+    }
+
+    public String getContentType()
+    {
+        return type;
+    }
+
+    public String getId()
+    {
+        return "workspace";
+    }
+
+    /**
+     * Gets the key of this workspace repository. The key is used to distinguish one workspace from another and should
+     * be sensitive to the artifacts that are (potentially) available in the workspace.
+     * 
+     * @return The (comparison) key for this workspace repository, never {@code null}.
+     */
+    public Object getKey()
+    {
+        return key;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "(" + getContentType() + ")";
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        WorkspaceRepository that = (WorkspaceRepository) obj;
+
+        return getContentType().equals( that.getContentType() ) && getKey().equals( that.getKey() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + getKey().hashCode();
+        hash = hash * 31 + getContentType().hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/package-info.java
new file mode 100644
index 0000000..538e7f1
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The definition of various kinds of repositories that host artifacts.
+ */
+package org.eclipse.aether.repository;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorException.java
new file mode 100644
index 0000000..d645a82
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorException.java
@@ -0,0 +1,91 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unreadable or unresolvable artifact descriptor.
+ */
+public class ArtifactDescriptorException
+    extends RepositoryException
+{
+
+    private final transient ArtifactDescriptorResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result )
+    {
+        super( "Failed to read artifact descriptor"
+            + ( result != null ? " for " + result.getRequest().getArtifact() : "" ), getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The descriptor result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactDescriptorException( ArtifactDescriptorResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the descriptor result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The descriptor result or {@code null} if unknown.
+     */
+    public ArtifactDescriptorResult getResult()
+    {
+        return result;
+    }
+
+    private static Throwable getCause( ArtifactDescriptorResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java
new file mode 100644
index 0000000..c4de9b2
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicy.java
@@ -0,0 +1,61 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Controls the handling of errors related to reading an artifact descriptor.
+ * 
+ * @see RepositorySystemSession#getArtifactDescriptorPolicy()
+ */
+public interface ArtifactDescriptorPolicy
+{
+
+    /**
+     * Bit mask indicating that errors while reading the artifact descriptor should not be tolerated.
+     */
+    int STRICT = 0x00;
+
+    /**
+     * Bit flag indicating that missing artifact descriptors should be silently ignored.
+     */
+    int IGNORE_MISSING = 0x01;
+
+    /**
+     * Bit flag indicating that existent but invalid artifact descriptors should be silently ignored.
+     */
+    int IGNORE_INVALID = 0x02;
+
+    /**
+     * Bit mask indicating that all errors should be silently ignored.
+     */
+    int IGNORE_ERRORS = IGNORE_MISSING | IGNORE_INVALID;
+
+    /**
+     * Gets the error policy for an artifact's descriptor.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getPolicy( RepositorySystemSession session, ArtifactDescriptorPolicyRequest request );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java
new file mode 100644
index 0000000..ffaac16
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorPolicyRequest.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A query for the error policy for a given artifact's descriptor.
+ * 
+ * @see ArtifactDescriptorPolicy
+ */
+public final class ArtifactDescriptorPolicyRequest
+{
+
+    private Artifact artifact;
+
+    private String context = "";
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactDescriptorPolicyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified artifact.
+     * 
+     * @param artifact The artifact for whose descriptor to determine the error policy, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest( Artifact artifact, String context )
+    {
+        setArtifact( artifact );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact for whose descriptor to determine the error policy.
+     * 
+     * @return The artifact for whose descriptor to determine the error policy or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact for whose descriptor to determine the error policy.
+     * 
+     * @param artifact The artifact for whose descriptor to determine the error policy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorPolicyRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getArtifact() );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java
new file mode 100644
index 0000000..387b1dc
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorRequest.java
@@ -0,0 +1,190 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to read an artifact descriptor.
+ * 
+ * @see RepositorySystem#readArtifactDescriptor(RepositorySystemSession, ArtifactDescriptorRequest)
+ */
+public final class ArtifactDescriptorRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactDescriptorRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose descriptor should be read, may be {@code null}.
+     * @param repositories The repositories to resolve the descriptor from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactDescriptorRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose descriptor shall be read.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose descriptor shall be read. Eventually, a valid request must have an artifact set.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the descriptor from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the descriptor from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution of the artifact descriptor.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorResult.java
new file mode 100644
index 0000000..3d0edd2
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactDescriptorResult.java
@@ -0,0 +1,463 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * The result from reading an artifact descriptor.
+ * 
+ * @see RepositorySystem#readArtifactDescriptor(RepositorySystemSession, ArtifactDescriptorRequest)
+ */
+public final class ArtifactDescriptorResult
+{
+
+    private final ArtifactDescriptorRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<Artifact> relocations;
+
+    private Collection<Artifact> aliases;
+
+    private Artifact artifact;
+
+    private ArtifactRepository repository;
+
+    private List<Dependency> dependencies;
+
+    private List<Dependency> managedDependencies;
+
+    private List<RemoteRepository> repositories;
+
+    private Map<String, Object> properties;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The descriptor request, must not be {@code null}.
+     */
+    public ArtifactDescriptorResult( ArtifactDescriptorRequest request )
+    {
+        this.request = requireNonNull( request, "artifact descriptor request cannot be null" );
+        artifact = request.getArtifact();
+        exceptions = Collections.emptyList();
+        relocations = Collections.emptyList();
+        aliases = Collections.emptyList();
+        dependencies = managedDependencies = Collections.emptyList();
+        repositories = Collections.emptyList();
+        properties = Collections.emptyMap();
+    }
+
+    /**
+     * Gets the descriptor request that was made.
+     * 
+     * @return The descriptor request, never {@code null}.
+     */
+    public ArtifactDescriptorRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while reading the artifact descriptor.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Sets the exceptions that occurred while reading the artifact descriptor.
+     * 
+     * @param exceptions The exceptions that occurred, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setExceptions( List<Exception> exceptions )
+    {
+        if ( exceptions == null )
+        {
+            this.exceptions = Collections.emptyList();
+        }
+        else
+        {
+            this.exceptions = exceptions;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified exception while reading the artifact descriptor.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the relocations that were processed to read the artifact descriptor. The returned list denotes the hops that
+     * lead to the final artifact coordinates as given by {@link #getArtifact()}.
+     * 
+     * @return The relocations that were processed, never {@code null}.
+     */
+    public List<Artifact> getRelocations()
+    {
+        return relocations;
+    }
+
+    /**
+     * Sets the relocations that were processed to read the artifact descriptor.
+     * 
+     * @param relocations The relocations that were processed, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRelocations( List<Artifact> relocations )
+    {
+        if ( relocations == null )
+        {
+            this.relocations = Collections.emptyList();
+        }
+        else
+        {
+            this.relocations = relocations;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified relocation hop while locating the artifact descriptor.
+     * 
+     * @param artifact The artifact that got relocated, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addRelocation( Artifact artifact )
+    {
+        if ( artifact != null )
+        {
+            if ( relocations.isEmpty() )
+            {
+                relocations = new ArrayList<Artifact>();
+            }
+            relocations.add( artifact );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the known aliases for this artifact. An alias denotes a different artifact with (almost) the same contents
+     * and can be used to mark a patched rebuild of some other artifact as such, thereby allowing conflict resolution to
+     * consider the patched and the original artifact as a conflict.
+     * 
+     * @return The aliases of the artifact, never {@code null}.
+     */
+    public Collection<Artifact> getAliases()
+    {
+        return aliases;
+    }
+
+    /**
+     * Sets the aliases of the artifact.
+     * 
+     * @param aliases The aliases of the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setAliases( Collection<Artifact> aliases )
+    {
+        if ( aliases == null )
+        {
+            this.aliases = Collections.emptyList();
+        }
+        else
+        {
+            this.aliases = aliases;
+        }
+        return this;
+    }
+
+    /**
+     * Records the specified alias.
+     * 
+     * @param alias The alias for the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addAlias( Artifact alias )
+    {
+        if ( alias != null )
+        {
+            if ( aliases.isEmpty() )
+            {
+                aliases = new ArrayList<Artifact>();
+            }
+            aliases.add( alias );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the artifact whose descriptor was read. This can be a different artifact than originally requested in case
+     * relocations were encountered.
+     * 
+     * @return The artifact after following any relocations, never {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose descriptor was read.
+     * 
+     * @param artifact The artifact whose descriptor was read, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the descriptor was eventually resolved.
+     * 
+     * @return The repository from which the descriptor was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the descriptor was resolved.
+     * 
+     * @param repository The repository from which the descriptor was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the list of direct dependencies of the artifact.
+     * 
+     * @return The list of direct dependencies, never {@code null}
+     */
+    public List<Dependency> getDependencies()
+    {
+        return dependencies;
+    }
+
+    /**
+     * Sets the list of direct dependencies of the artifact.
+     * 
+     * @param dependencies The list of direct dependencies, may be {@code null}
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.dependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.dependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified direct dependency.
+     * 
+     * @param dependency The direct dependency to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( dependencies.isEmpty() )
+            {
+                dependencies = new ArrayList<Dependency>();
+            }
+            dependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the dependency management information.
+     * 
+     * @return The dependency management information.
+     */
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    /**
+     * Sets the dependency management information.
+     * 
+     * @param dependencies The dependency management information, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setManagedDependencies( List<Dependency> dependencies )
+    {
+        if ( dependencies == null )
+        {
+            this.managedDependencies = Collections.emptyList();
+        }
+        else
+        {
+            this.managedDependencies = dependencies;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified managed dependency.
+     * 
+     * @param dependency The managed dependency to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addManagedDependency( Dependency dependency )
+    {
+        if ( dependency != null )
+        {
+            if ( managedDependencies.isEmpty() )
+            {
+                managedDependencies = new ArrayList<Dependency>();
+            }
+            managedDependencies.add( dependency );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories listed in the artifact descriptor.
+     * 
+     * @return The remote repositories listed in the artifact descriptor, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories listed in the artifact descriptor.
+     * 
+     * @param repositories The remote repositories listed in the artifact descriptor, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified remote repository.
+     * 
+     * @param repository The remote repository to add, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( repositories.isEmpty() )
+            {
+                repositories = new ArrayList<RemoteRepository>();
+            }
+            repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets any additional information about the artifact in form of key-value pairs. <em>Note:</em> Regardless of their
+     * actual type, all property values must be treated as being read-only.
+     * 
+     * @return The additional information about the artifact, never {@code null}.
+     */
+    public Map<String, Object> getProperties()
+    {
+        return properties;
+    }
+
+    /**
+     * Sets any additional information about the artifact in form of key-value pairs.
+     * 
+     * @param properties The additional information about the artifact, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactDescriptorResult setProperties( Map<String, Object> properties )
+    {
+        if ( properties == null )
+        {
+            this.properties = Collections.emptyMap();
+        }
+        else
+        {
+            this.properties = properties;
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " -> " + getDependencies();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactRequest.java
new file mode 100644
index 0000000..a220207
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactRequest.java
@@ -0,0 +1,232 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve an artifact.
+ * 
+ * @see RepositorySystem#resolveArtifacts(RepositorySystemSession, java.util.Collection)
+ * @see Artifact#getFile()
+ */
+public final class ArtifactRequest
+{
+
+    private Artifact artifact;
+
+    private DependencyNode node;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ArtifactRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact to resolve, may be {@code null}.
+     * @param repositories The repositories to resolve the artifact from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public ArtifactRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Creates a request from the specified dependency node.
+     * 
+     * @param node The dependency node to resolve, may be {@code null}.
+     */
+    public ArtifactRequest( DependencyNode node )
+    {
+        setDependencyNode( node );
+        setRepositories( node.getRepositories() );
+        setRequestContext( node.getRequestContext() );
+    }
+
+    /**
+     * Gets the artifact to resolve.
+     * 
+     * @return The artifact to resolve or {@code null}.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to resolve.
+     * 
+     * @param artifact The artifact to resolve, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the dependency node (if any) for which to resolve the artifact.
+     * 
+     * @return The dependency node to resolve or {@code null} if unknown.
+     */
+    public DependencyNode getDependencyNode()
+    {
+        return node;
+    }
+
+    /**
+     * Sets the dependency node to resolve.
+     * 
+     * @param node The dependency node to resolve, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setDependencyNode( DependencyNode node )
+    {
+        this.node = node;
+        if ( node != null )
+        {
+            setArtifact( node.getDependency().getArtifact() );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the artifact from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the artifact from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ArtifactRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java
new file mode 100644
index 0000000..bfae4a0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java
@@ -0,0 +1,173 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+
+/**
+ * Thrown in case of a unresolvable artifacts.
+ */
+public class ArtifactResolutionException
+    extends RepositoryException
+{
+
+    private final transient List<ArtifactResult> results;
+
+    /**
+     * Creates a new exception with the specified results.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results )
+    {
+        super( getMessage( results ), getCause( results ) );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult>emptyList();
+    }
+
+    /**
+     * Creates a new exception with the specified results and detail message.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results, String message )
+    {
+        super( message, getCause( results ) );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult>emptyList();
+    }
+
+    /**
+     * Creates a new exception with the specified results, detail message and cause.
+     * 
+     * @param results The resolution results at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactResolutionException( List<ArtifactResult> results, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.results = ( results != null ) ? results : Collections.<ArtifactResult>emptyList();
+    }
+
+    /**
+     * Gets the resolution results at the point the exception occurred. Despite being incomplete, callers might want to
+     * use these results to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The resolution results or {@code null} if unknown.
+     */
+    public List<ArtifactResult> getResults()
+    {
+        return results;
+    }
+
+    /**
+     * Gets the first result from {@link #getResults()}. This is a convenience method for cases where callers know only
+     * a single result/request is involved.
+     * 
+     * @return The (first) resolution result or {@code null} if none.
+     */
+    public ArtifactResult getResult()
+    {
+        return ( results != null && !results.isEmpty() ) ? results.get( 0 ) : null;
+    }
+
+    private static String getMessage( List<? extends ArtifactResult> results )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+
+        buffer.append( "The following artifacts could not be resolved: " );
+
+        int unresolved = 0;
+
+        String sep = "";
+        for ( ArtifactResult result : results )
+        {
+            if ( !result.isResolved() )
+            {
+                unresolved++;
+
+                buffer.append( sep );
+                buffer.append( result.getRequest().getArtifact() );
+                sep = ", ";
+            }
+        }
+
+        Throwable cause = getCause( results );
+        if ( cause != null )
+        {
+            if ( unresolved == 1 )
+            {
+                buffer.setLength( 0 );
+                buffer.append( cause.getMessage() );
+            }
+            else
+            {
+                buffer.append( ": " ).append( cause.getMessage() );
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( List<? extends ArtifactResult> results )
+    {
+        for ( ArtifactResult result : results )
+        {
+            if ( !result.isResolved() )
+            {
+                Throwable notFound = null, offline = null;
+                for ( Throwable t : result.getExceptions() )
+                {
+                    if ( t instanceof ArtifactNotFoundException )
+                    {
+                        if ( notFound == null )
+                        {
+                            notFound = t;
+                        }
+                        if ( offline == null && t.getCause() instanceof RepositoryOfflineException )
+                        {
+                            offline = t;
+                        }
+                    }
+                    else
+                    {
+                        return t;
+                    }
+
+                }
+                if ( offline != null )
+                {
+                    return offline;
+                }
+                if ( notFound != null )
+                {
+                    return notFound;
+                }
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java
new file mode 100644
index 0000000..5057855
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java
@@ -0,0 +1,185 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+
+/**
+ * The result of an artifact resolution request.
+ * 
+ * @see RepositorySystem#resolveArtifacts(RepositorySystemSession, java.util.Collection)
+ * @see Artifact#getFile()
+ */
+public final class ArtifactResult
+{
+
+    private final ArtifactRequest request;
+
+    private List<Exception> exceptions;
+
+    private Artifact artifact;
+
+    private ArtifactRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public ArtifactResult( ArtifactRequest request )
+    {
+        this.request = requireNonNull( request, "artifact request cannot be null" );
+        exceptions = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     *
+     * @return The resolution request, never {@code null}.
+     */
+    public ArtifactRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the resolved artifact (if any). Use {@link #getExceptions()} to query the errors that occurred while trying
+     * to resolve the artifact.
+     * 
+     * @return The resolved artifact or {@code null} if the resolution failed.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the resolved artifact.
+     * 
+     * @param artifact The resolved artifact, may be {@code null} if the resolution failed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the artifact. Note that this list can be non-empty even if the
+     * artifact was successfully resolved, e.g. when one of the contacted remote repositories didn't contain the
+     * artifact but a later repository eventually contained it.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     * @see #isResolved()
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the artifact.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the artifact was eventually resolved. Note that successive resolutions of the same
+     * artifact might yield different results if the employed local repository does not track the origin of an artifact.
+     * 
+     * @return The repository from which the artifact was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the artifact was resolved.
+     * 
+     * @param repository The repository from which the artifact was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public ArtifactResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Indicates whether the requested artifact was resolved. Note that the artifact might have been successfully
+     * resolved despite {@link #getExceptions()} indicating transfer errors while trying to fetch the artifact from some
+     * of the specified remote repositories.
+     * 
+     * @return {@code true} if the artifact was resolved, {@code false} otherwise.
+     * @see Artifact#getFile()
+     */
+    public boolean isResolved()
+    {
+        return getArtifact() != null && getArtifact().getFile() != null;
+    }
+
+    /**
+     * Indicates whether the requested artifact is not present in any of the specified repositories.
+     * 
+     * @return {@code true} if the artifact is not present in any repository, {@code false} otherwise.
+     */
+    public boolean isMissing()
+    {
+        for ( Exception e : getExceptions() )
+        {
+            if ( !( e instanceof ArtifactNotFoundException ) )
+            {
+                return false;
+            }
+        }
+        return !isResolved();
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyRequest.java
new file mode 100644
index 0000000..138304a
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyRequest.java
@@ -0,0 +1,187 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A request to resolve transitive dependencies. This request can either be supplied with a {@link CollectRequest} to
+ * calculate the transitive dependencies or with an already resolved dependency graph.
+ * 
+ * @see RepositorySystem#resolveDependencies(RepositorySystemSession, DependencyRequest)
+ * @see Artifact#getFile()
+ */
+public final class DependencyRequest
+{
+
+    private DependencyNode root;
+
+    private CollectRequest collectRequest;
+
+    private DependencyFilter filter;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request. Note that either {@link #setRoot(DependencyNode)} or
+     * {@link #setCollectRequest(CollectRequest)} must eventually be called to create a valid request.
+     */
+    public DependencyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified dependency graph and with the given resolution filter.
+     * 
+     * @param node The root node of the dependency graph whose artifacts should be resolved, may be {@code null}.
+     * @param filter The resolution filter to use, may be {@code null}.
+     */
+    public DependencyRequest( DependencyNode node, DependencyFilter filter )
+    {
+        setRoot( node );
+        setFilter( filter );
+    }
+
+    /**
+     * Creates a request for the specified collect request and with the given resolution filter.
+     * 
+     * @param request The collect request used to calculate the dependency graph whose artifacts should be resolved, may
+     *            be {@code null}.
+     * @param filter The resolution filter to use, may be {@code null}.
+     */
+    public DependencyRequest( CollectRequest request, DependencyFilter filter )
+    {
+        setCollectRequest( request );
+        setFilter( filter );
+    }
+
+    /**
+     * Gets the root node of the dependency graph whose artifacts should be resolved.
+     * 
+     * @return The root node of the dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the dependency graph whose artifacts should be resolved. When this request is processed,
+     * the nodes of the given dependency graph will be updated to refer to the resolved artifacts. Eventually, either
+     * {@link #setRoot(DependencyNode)} or {@link #setCollectRequest(CollectRequest)} must be called to create a valid
+     * request.
+     * 
+     * @param root The root node of the dependency graph, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the collect request used to calculate the dependency graph whose artifacts should be resolved.
+     * 
+     * @return The collect request or {@code null} if none.
+     */
+    public CollectRequest getCollectRequest()
+    {
+        return collectRequest;
+    }
+
+    /**
+     * Sets the collect request used to calculate the dependency graph whose artifacts should be resolved. Eventually,
+     * either {@link #setRoot(DependencyNode)} or {@link #setCollectRequest(CollectRequest)} must be called to create a
+     * valid request. If this request is supplied with a dependency node via {@link #setRoot(DependencyNode)}, the
+     * collect request is ignored.
+     * 
+     * @param collectRequest The collect request, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setCollectRequest( CollectRequest collectRequest )
+    {
+        this.collectRequest = collectRequest;
+        return this;
+    }
+
+    /**
+     * Gets the resolution filter used to select which artifacts of the dependency graph should be resolved.
+     * 
+     * @return The resolution filter or {@code null} to resolve all artifacts of the dependency graph.
+     */
+    public DependencyFilter getFilter()
+    {
+        return filter;
+    }
+
+    /**
+     * Sets the resolution filter used to select which artifacts of the dependency graph should be resolved. For
+     * example, use this filter to restrict resolution to dependencies of a certain scope.
+     * 
+     * @param filter The resolution filter, may be {@code null} to resolve all artifacts of the dependency graph.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setFilter( DependencyFilter filter )
+    {
+        this.filter = filter;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public DependencyRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        if ( root != null )
+        {
+            return String.valueOf( root );
+        }
+        return String.valueOf( collectRequest );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResolutionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResolutionException.java
new file mode 100644
index 0000000..2c12b57
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResolutionException.java
@@ -0,0 +1,83 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a unresolvable dependencies.
+ */
+public class DependencyResolutionException
+    extends RepositoryException
+{
+
+    private final transient DependencyResult result;
+
+    /**
+     * Creates a new exception with the specified result and cause.
+     * 
+     * @param result The dependency result at the point the exception occurred, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyResolutionException( DependencyResult result, Throwable cause )
+    {
+        super( getMessage( cause ), cause );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The dependency result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public DependencyResolutionException( DependencyResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    private static String getMessage( Throwable cause )
+    {
+        String msg = null;
+        if ( cause != null )
+        {
+            msg = cause.getMessage();
+        }
+        if ( msg == null || msg.length() <= 0 )
+        {
+            msg = "Could not resolve transitive dependencies";
+        }
+        return msg;
+    }
+
+    /**
+     * Gets the dependency result at the point the exception occurred. Despite being incomplete, callers might want to
+     * use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The dependency result or {@code null} if unknown.
+     */
+    public DependencyResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResult.java
new file mode 100644
index 0000000..8ba8646
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/DependencyResult.java
@@ -0,0 +1,192 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * The result of a dependency resolution request.
+ * 
+ * @see RepositorySystem#resolveDependencies(RepositorySystemSession, DependencyRequest)
+ */
+public final class DependencyResult
+{
+
+    private final DependencyRequest request;
+
+    private DependencyNode root;
+
+    private List<DependencyCycle> cycles;
+
+    private List<Exception> collectExceptions;
+
+    private List<ArtifactResult> artifactResults;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public DependencyResult( DependencyRequest request )
+    {
+        this.request = requireNonNull( request, "dependency request cannot be null" );
+        root = request.getRoot();
+        cycles = Collections.emptyList();
+        collectExceptions = Collections.emptyList();
+        artifactResults = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public DependencyRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the root node of the resolved dependency graph. Note that this dependency graph might be
+     * incomplete/unfinished in case of {@link #getCollectExceptions()} indicating errors during its calculation.
+     * 
+     * @return The root node of the resolved dependency graph or {@code null} if none.
+     */
+    public DependencyNode getRoot()
+    {
+        return root;
+    }
+
+    /**
+     * Sets the root node of the resolved dependency graph.
+     * 
+     * @param root The root node of the resolved dependency graph, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setRoot( DependencyNode root )
+    {
+        this.root = root;
+        return this;
+    }
+
+    /**
+     * Gets the dependency cycles that were encountered while building the dependency graph. Note that dependency cycles
+     * will only be reported here if the underlying request was created from a
+     * {@link org.eclipse.aether.collection.CollectRequest CollectRequest}. If the underlying {@link DependencyRequest}
+     * was created from an existing dependency graph, information about cycles will not be available in this result.
+     * 
+     * @return The dependency cycles in the (raw) graph, never {@code null}.
+     */
+    public List<DependencyCycle> getCycles()
+    {
+        return cycles;
+    }
+
+    /**
+     * Records the specified dependency cycles while building the dependency graph.
+     * 
+     * @param cycles The dependency cycles to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setCycles( List<DependencyCycle> cycles )
+    {
+        if ( cycles == null )
+        {
+            this.cycles = Collections.emptyList();
+        }
+        else
+        {
+            this.cycles = cycles;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the exceptions that occurred while building the dependency graph.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getCollectExceptions()
+    {
+        return collectExceptions;
+    }
+
+    /**
+     * Records the specified exceptions while building the dependency graph.
+     * 
+     * @param exceptions The exceptions to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setCollectExceptions( List<Exception> exceptions )
+    {
+        if ( exceptions == null )
+        {
+            this.collectExceptions = Collections.emptyList();
+        }
+        else
+        {
+            this.collectExceptions = exceptions;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the resolution results for the dependency artifacts that matched {@link DependencyRequest#getFilter()}.
+     * 
+     * @return The resolution results for the dependency artifacts, never {@code null}.
+     */
+    public List<ArtifactResult> getArtifactResults()
+    {
+        return artifactResults;
+    }
+
+    /**
+     * Sets the resolution results for the artifacts that matched {@link DependencyRequest#getFilter()}.
+     * 
+     * @param results The resolution results for the artifacts, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public DependencyResult setArtifactResults( List<ArtifactResult> results )
+    {
+        if ( results == null )
+        {
+            this.artifactResults = Collections.emptyList();
+        }
+        else
+        {
+            this.artifactResults = results;
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( artifactResults );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataRequest.java
new file mode 100644
index 0000000..86063ff
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataRequest.java
@@ -0,0 +1,232 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve metadata from either a remote repository or the local repository.
+ * 
+ * @see RepositorySystem#resolveMetadata(RepositorySystemSession, java.util.Collection)
+ * @see Metadata#getFile()
+ */
+public final class MetadataRequest
+{
+
+    private Metadata metadata;
+
+    private RemoteRepository repository;
+
+    private String context = "";
+
+    private boolean deleteLocalCopyIfMissing;
+
+    private boolean favorLocalRepository;
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public MetadataRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request to resolve the specified metadata from the local repository.
+     * 
+     * @param metadata The metadata to resolve, may be {@code null}.
+     */
+    public MetadataRequest( Metadata metadata )
+    {
+        setMetadata( metadata );
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param metadata The metadata to resolve, may be {@code null}.
+     * @param repository The repository to resolve the metadata from, may be {@code null} to resolve from the local
+     *            repository.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public MetadataRequest( Metadata metadata, RemoteRepository repository, String context )
+    {
+        setMetadata( metadata );
+        setRepository( repository );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the metadata to resolve.
+     * 
+     * @return The metadata or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to resolve.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the metadata should be resolved.
+     * 
+     * @return The repository or {@code null} to resolve from the local repository.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the metadata should be resolved.
+     * 
+     * @param repository The repository, may be {@code null} to resolve from the local repository.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Indicates whether the locally cached copy of the metadata should be removed if the corresponding file does not
+     * exist (any more) in the remote repository.
+     * 
+     * @return {@code true} if locally cached metadata should be deleted if no corresponding remote file exists,
+     *         {@code false} to keep the local copy.
+     */
+    public boolean isDeleteLocalCopyIfMissing()
+    {
+        return deleteLocalCopyIfMissing;
+    }
+
+    /**
+     * Controls whether the locally cached copy of the metadata should be removed if the corresponding file does not
+     * exist (any more) in the remote repository.
+     * 
+     * @param deleteLocalCopyIfMissing {@code true} if locally cached metadata should be deleted if no corresponding
+     *            remote file exists, {@code false} to keep the local copy.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setDeleteLocalCopyIfMissing( boolean deleteLocalCopyIfMissing )
+    {
+        this.deleteLocalCopyIfMissing = deleteLocalCopyIfMissing;
+        return this;
+    }
+
+    /**
+     * Indicates whether the metadata resolution should be suppressed if the corresponding metadata of the local
+     * repository is up-to-date according to the update policy of the remote repository. In this case, the metadata
+     * resolution will even be suppressed if no local copy of the remote metadata exists yet.
+     * 
+     * @return {@code true} to suppress resolution of remote metadata if the corresponding metadata of the local
+     *         repository is up-to-date, {@code false} to resolve the remote metadata normally according to the update
+     *         policy.
+     */
+    public boolean isFavorLocalRepository()
+    {
+        return favorLocalRepository;
+    }
+
+    /**
+     * Controls resolution of remote metadata when already corresponding metadata of the local repository exists. In
+     * cases where the local repository's metadata is sufficient and going to be preferred, resolution of the remote
+     * metadata can be suppressed to avoid unnecessary network access.
+     * 
+     * @param favorLocalRepository {@code true} to suppress resolution of remote metadata if the corresponding metadata
+     *            of the local repository is up-to-date, {@code false} to resolve the remote metadata normally according
+     *            to the update policy.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setFavorLocalRepository( boolean favorLocalRepository )
+    {
+        this.favorLocalRepository = favorLocalRepository;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public MetadataRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " < " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java
new file mode 100644
index 0000000..cedd395
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java
@@ -0,0 +1,164 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+
+/**
+ * The result of a metadata resolution request.
+ * 
+ * @see RepositorySystem#resolveMetadata(RepositorySystemSession, java.util.Collection)
+ */
+public final class MetadataResult
+{
+
+    private final MetadataRequest request;
+
+    private Exception exception;
+
+    private boolean updated;
+
+    private Metadata metadata;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public MetadataResult( MetadataRequest request )
+    {
+        this.request = requireNonNull( request, "metadata request cannot be null" );
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     *
+     * @return The resolution request, never {@code null}.
+     */
+    public MetadataRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the resolved metadata (if any).
+     * 
+     * @return The resolved metadata or {@code null} if the resolution failed.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the resolved metadata.
+     * 
+     * @param metadata The resolved metadata, may be {@code null} if the resolution failed.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Records the specified exception while resolving the metadata.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setException( Exception exception )
+    {
+        this.exception = exception;
+        return this;
+    }
+
+    /**
+     * Gets the exception that occurred while resolving the metadata.
+     * 
+     * @return The exception that occurred or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exception;
+    }
+
+    /**
+     * Sets the updated flag for the metadata.
+     * 
+     * @param updated {@code true} if the metadata was actually fetched from the remote repository during the
+     *            resolution, {@code false} if the metadata was resolved from a locally cached copy.
+     * @return This result for chaining, never {@code null}.
+     */
+    public MetadataResult setUpdated( boolean updated )
+    {
+        this.updated = updated;
+        return this;
+    }
+
+    /**
+     * Indicates whether the metadata was actually fetched from the remote repository or resolved from the local cache.
+     * If metadata has been locally cached during a previous resolution request and this local copy is still up-to-date
+     * according to the remote repository's update policy, no remote access is made.
+     * 
+     * @return {@code true} if the metadata was actually fetched from the remote repository during the resolution,
+     *         {@code false} if the metadata was resolved from a locally cached copy.
+     */
+    public boolean isUpdated()
+    {
+        return updated;
+    }
+
+    /**
+     * Indicates whether the requested metadata was resolved. Note that the metadata might have been successfully
+     * resolved (from the local cache) despite {@link #getException()} indicating a transfer error while trying to
+     * refetch the metadata from the remote repository.
+     * 
+     * @return {@code true} if the metadata was resolved, {@code false} otherwise.
+     * @see Metadata#getFile()
+     */
+    public boolean isResolved()
+    {
+        return getMetadata() != null && getMetadata().getFile() != null;
+    }
+
+    /**
+     * Indicates whether the requested metadata is not present in the remote repository.
+     * 
+     * @return {@code true} if the metadata is not present in the remote repository, {@code false} otherwise.
+     */
+    public boolean isMissing()
+    {
+        return getException() instanceof MetadataNotFoundException;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + ( isUpdated() ? " (updated)" : " (cached)" );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicy.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicy.java
new file mode 100644
index 0000000..5158fa0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicy.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * Controls the caching of resolution errors for artifacts/metadata from remote repositories. If caching is enabled for
+ * a given resource, a marker will be set (usually somewhere in the local repository) to suppress repeated resolution
+ * attempts for the broken resource, thereby avoiding expensive but useless network IO. The error marker is considered
+ * stale once the repository's update policy has expired at which point a future resolution attempt will be allowed.
+ * Error caching considers the current network settings such that fixes to the configuration like authentication or
+ * proxy automatically trigger revalidation with the remote side regardless of the time elapsed since the previous
+ * resolution error.
+ * 
+ * @see RepositorySystemSession#getResolutionErrorPolicy()
+ */
+public interface ResolutionErrorPolicy
+{
+
+    /**
+     * Bit mask indicating that resolution errors should not be cached in the local repository. This forces the system
+     * to always query the remote repository for locally missing artifacts/metadata.
+     */
+    int CACHE_DISABLED = 0x00;
+
+    /**
+     * Bit flag indicating whether missing artifacts/metadata should be cached in the local repository. If caching is
+     * enabled, resolution will not be reattempted until the update policy for the affected resource has expired.
+     */
+    int CACHE_NOT_FOUND = 0x01;
+
+    /**
+     * Bit flag indicating whether connectivity/transfer errors (e.g. unreachable host, bad authentication) should be
+     * cached in the local repository. If caching is enabled, resolution will not be reattempted until the update policy
+     * for the affected resource has expired.
+     */
+    int CACHE_TRANSFER_ERROR = 0x02;
+
+    /**
+     * Bit mask indicating that all resolution errors should be cached in the local repository.
+     */
+    int CACHE_ALL = CACHE_NOT_FOUND | CACHE_TRANSFER_ERROR;
+
+    /**
+     * Gets the error policy for an artifact.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getArtifactPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Artifact> request );
+
+    /**
+     * Gets the error policy for some metadata.
+     * 
+     * @param session The repository session during which the policy is determined, must not be {@code null}.
+     * @param request The policy request holding further details, must not be {@code null}.
+     * @return The bit mask describing the desired error policy.
+     */
+    int getMetadataPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Metadata> request );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java
new file mode 100644
index 0000000..9126914
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ResolutionErrorPolicyRequest.java
@@ -0,0 +1,107 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A query for the resolution error policy for a given artifact/metadata.
+ * 
+ * @param <T> The type of the affected repository item (artifact or metadata).
+ * @see ResolutionErrorPolicy
+ */
+public final class ResolutionErrorPolicyRequest<T>
+{
+
+    private T item;
+
+    private RemoteRepository repository;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public ResolutionErrorPolicyRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request for the specified artifact/metadata and remote repository.
+     * 
+     * @param item The artifact/metadata for which to determine the error policy, may be {@code null}.
+     * @param repository The repository from which the resolution is attempted, may be {@code null}.
+     */
+    public ResolutionErrorPolicyRequest( T item, RemoteRepository repository )
+    {
+        setItem( item );
+        setRepository( repository );
+    }
+
+    /**
+     * Gets the artifact/metadata for which to determine the error policy.
+     * 
+     * @return The artifact/metadata for which to determine the error policy or {@code null} if not set.
+     */
+    public T getItem()
+    {
+        return item;
+    }
+
+    /**
+     * Sets the artifact/metadata for which to determine the error policy.
+     * 
+     * @param item The artifact/metadata for which to determine the error policy, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ResolutionErrorPolicyRequest<T> setItem( T item )
+    {
+        this.item = item;
+        return this;
+    }
+
+    /**
+     * Gets the remote repository from which the resolution of the artifact/metadata is attempted.
+     * 
+     * @return The involved remote repository or {@code null} if not set.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the remote repository from which the resolution of the artifact/metadata is attempted.
+     * 
+     * @param repository The repository from which the resolution is attempted, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public ResolutionErrorPolicyRequest<T> setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getItem() + " < " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeRequest.java
new file mode 100644
index 0000000..d6aa16b
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeRequest.java
@@ -0,0 +1,190 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve a version range.
+ * 
+ * @see RepositorySystem#resolveVersionRange(RepositorySystemSession, VersionRangeRequest)
+ */
+public final class VersionRangeRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public VersionRangeRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose version range should be resolved, may be {@code null}.
+     * @param repositories The repositories to resolve the version from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public VersionRangeRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose version range shall be resolved.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose version range shall be resolved.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the version range from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the version range from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRangeRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResolutionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResolutionException.java
new file mode 100644
index 0000000..deb0e52
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResolutionException.java
@@ -0,0 +1,105 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unparseable or unresolvable version range.
+ */
+public class VersionRangeResolutionException
+    extends RepositoryException
+{
+
+    private final transient VersionRangeResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result )
+    {
+        super( getMessage( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    private static String getMessage( VersionRangeResult result )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Failed to resolve version range" );
+        if ( result != null )
+        {
+            buffer.append( " for " ).append( result.getRequest().getArtifact() );
+            if ( !result.getExceptions().isEmpty() )
+            {
+                buffer.append( ": " ).append( result.getExceptions().iterator().next().getMessage() );
+            }
+        }
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( VersionRangeResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result, String message )
+    {
+        super( message );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The version range result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public VersionRangeResolutionException( VersionRangeResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the version range result at the point the exception occurred. Despite being incomplete, callers might want
+     * to use this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The version range result or {@code null} if unknown.
+     */
+    public VersionRangeResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResult.java
new file mode 100644
index 0000000..8af78ea
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRangeResult.java
@@ -0,0 +1,237 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * The result of a version range resolution request.
+ * 
+ * @see RepositorySystem#resolveVersionRange(RepositorySystemSession, VersionRangeRequest)
+ */
+public final class VersionRangeResult
+{
+
+    private final VersionRangeRequest request;
+
+    private List<Exception> exceptions;
+
+    private List<Version> versions;
+
+    private Map<Version, ArtifactRepository> repositories;
+
+    private VersionConstraint versionConstraint;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public VersionRangeResult( VersionRangeRequest request )
+    {
+        this.request = requireNonNull( request, "version range request cannot be null" );
+        exceptions = Collections.emptyList();
+        versions = Collections.emptyList();
+        repositories = Collections.emptyMap();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     * 
+     * @return The resolution request, never {@code null}.
+     */
+    public VersionRangeRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the version range.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the version range.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the versions (in ascending order) that matched the requested range.
+     * 
+     * @return The matching versions (if any), never {@code null}.
+     */
+    public List<Version> getVersions()
+    {
+        return versions;
+    }
+
+    /**
+     * Adds the specified version to the result. Note that versions must be added in ascending order.
+     * 
+     * @param version The version to add, must not be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult addVersion( Version version )
+    {
+        if ( versions.isEmpty() )
+        {
+            versions = new ArrayList<Version>();
+        }
+        versions.add( version );
+        return this;
+    }
+
+    /**
+     * Sets the versions (in ascending order) matching the requested range.
+     * 
+     * @param versions The matching versions, may be empty or {@code null} if none.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setVersions( List<Version> versions )
+    {
+        if ( versions == null )
+        {
+            this.versions = Collections.emptyList();
+        }
+        else
+        {
+            this.versions = versions;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the lowest version matching the requested range.
+     * 
+     * @return The lowest matching version or {@code null} if no versions matched the requested range.
+     */
+    public Version getLowestVersion()
+    {
+        if ( versions.isEmpty() )
+        {
+            return null;
+        }
+        return versions.get( 0 );
+    }
+
+    /**
+     * Gets the highest version matching the requested range.
+     * 
+     * @return The highest matching version or {@code null} if no versions matched the requested range.
+     */
+    public Version getHighestVersion()
+    {
+        if ( versions.isEmpty() )
+        {
+            return null;
+        }
+        return versions.get( versions.size() - 1 );
+    }
+
+    /**
+     * Gets the repository from which the specified version was resolved.
+     * 
+     * @param version The version whose source repository should be retrieved, must not be {@code null}.
+     * @return The repository from which the version was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository( Version version )
+    {
+        return repositories.get( version );
+    }
+
+    /**
+     * Records the repository from which the specified version was resolved
+     * 
+     * @param version The version whose source repository is to be recorded, must not be {@code null}.
+     * @param repository The repository from which the version was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setRepository( Version version, ArtifactRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( repositories.isEmpty() )
+            {
+                repositories = new HashMap<Version, ArtifactRepository>();
+            }
+            repositories.put( version, repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the version constraint that was parsed from the artifact's version string.
+     * 
+     * @return The parsed version constraint or {@code null}.
+     */
+    public VersionConstraint getVersionConstraint()
+    {
+        return versionConstraint;
+    }
+
+    /**
+     * Sets the version constraint that was parsed from the artifact's version string.
+     * 
+     * @param versionConstraint The parsed version constraint, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionRangeResult setVersionConstraint( VersionConstraint versionConstraint )
+    {
+        this.versionConstraint = versionConstraint;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( repositories );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRequest.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRequest.java
new file mode 100644
index 0000000..3dde7dd
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionRequest.java
@@ -0,0 +1,190 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to resolve a metaversion.
+ * 
+ * @see RepositorySystem#resolveVersion(RepositorySystemSession, VersionRequest)
+ */
+public final class VersionRequest
+{
+
+    private Artifact artifact;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    private String context = "";
+
+    private RequestTrace trace;
+
+    /**
+     * Creates an uninitialized request.
+     */
+    public VersionRequest()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a request with the specified properties.
+     * 
+     * @param artifact The artifact whose (meta-)version should be resolved, may be {@code null}.
+     * @param repositories The repositories to resolve the version from, may be {@code null}.
+     * @param context The context in which this request is made, may be {@code null}.
+     */
+    public VersionRequest( Artifact artifact, List<RemoteRepository> repositories, String context )
+    {
+        setArtifact( artifact );
+        setRepositories( repositories );
+        setRequestContext( context );
+    }
+
+    /**
+     * Gets the artifact whose (meta-)version shall be resolved.
+     * 
+     * @return The artifact or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact whose (meta-)version shall be resolved.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the repositories to resolve the version from.
+     * 
+     * @return The repositories, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the repositories to resolve the version from.
+     * 
+     * @param repositories The repositories, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    /**
+     * Adds the specified repository for the resolution.
+     * 
+     * @param repository The repository to add, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest addRepository( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            if ( this.repositories.isEmpty() )
+            {
+                this.repositories = new ArrayList<RemoteRepository>();
+            }
+            this.repositories.add( repository );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the context in which this request is made.
+     * 
+     * @return The context, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context in which this request is made.
+     * 
+     * @param context The context, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this request is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This request for chaining, never {@code null}.
+     */
+    public VersionRequest setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " < " + getRepositories();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResolutionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResolutionException.java
new file mode 100644
index 0000000..1aca861
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResolutionException.java
@@ -0,0 +1,105 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of an unresolvable metaversion.
+ */
+public class VersionResolutionException
+    extends RepositoryException
+{
+
+    private final transient VersionResult result;
+
+    /**
+     * Creates a new exception with the specified result.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result )
+    {
+        super( getMessage( result ), getCause( result ) );
+        this.result = result;
+    }
+
+    private static String getMessage( VersionResult result )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Failed to resolve version" );
+        if ( result != null )
+        {
+            buffer.append( " for " ).append( result.getRequest().getArtifact() );
+            if ( !result.getExceptions().isEmpty() )
+            {
+                buffer.append( ": " ).append( result.getExceptions().iterator().next().getMessage() );
+            }
+        }
+        return buffer.toString();
+    }
+
+    private static Throwable getCause( VersionResult result )
+    {
+        Throwable cause = null;
+        if ( result != null && !result.getExceptions().isEmpty() )
+        {
+            cause = result.getExceptions().get( 0 );
+        }
+        return cause;
+    }
+
+    /**
+     * Creates a new exception with the specified result and detail message.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result, String message )
+    {
+        super( message, getCause( result ) );
+        this.result = result;
+    }
+
+    /**
+     * Creates a new exception with the specified result, detail message and cause.
+     * 
+     * @param result The version result at the point the exception occurred, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public VersionResolutionException( VersionResult result, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.result = result;
+    }
+
+    /**
+     * Gets the version result at the point the exception occurred. Despite being incomplete, callers might want to use
+     * this result to fail gracefully and continue their operation with whatever interim data has been gathered.
+     * 
+     * @return The version result or {@code null} if unknown.
+     */
+    public VersionResult getResult()
+    {
+        return result;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResult.java
new file mode 100644
index 0000000..486a287
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/VersionResult.java
@@ -0,0 +1,147 @@
+package org.eclipse.aether.resolution;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.ArtifactRepository;
+
+/**
+ * The result of a version resolution request.
+ * 
+ * @see RepositorySystem#resolveVersion(RepositorySystemSession, VersionRequest)
+ */
+public final class VersionResult
+{
+
+    private final VersionRequest request;
+
+    private List<Exception> exceptions;
+
+    private String version;
+
+    private ArtifactRepository repository;
+
+    /**
+     * Creates a new result for the specified request.
+     *
+     * @param request The resolution request, must not be {@code null}.
+     */
+    public VersionResult( VersionRequest request )
+    {
+        this.request = requireNonNull( request, "version request cannot be null" );
+        exceptions = Collections.emptyList();
+    }
+
+    /**
+     * Gets the resolution request that was made.
+     *
+     * @return The resolution request, never {@code null}.
+     */
+    public VersionRequest getRequest()
+    {
+        return request;
+    }
+
+    /**
+     * Gets the exceptions that occurred while resolving the version.
+     * 
+     * @return The exceptions that occurred, never {@code null}.
+     */
+    public List<Exception> getExceptions()
+    {
+        return exceptions;
+    }
+
+    /**
+     * Records the specified exception while resolving the version.
+     * 
+     * @param exception The exception to record, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult addException( Exception exception )
+    {
+        if ( exception != null )
+        {
+            if ( exceptions.isEmpty() )
+            {
+                exceptions = new ArrayList<Exception>();
+            }
+            exceptions.add( exception );
+        }
+        return this;
+    }
+
+    /**
+     * Gets the resolved version.
+     * 
+     * @return The resolved version or {@code null} if the resolution failed.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+    /**
+     * Sets the resolved version.
+     * 
+     * @param version The resolved version, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult setVersion( String version )
+    {
+        this.version = version;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which the version was eventually resolved.
+     * 
+     * @return The repository from which the version was resolved or {@code null} if unknown.
+     */
+    public ArtifactRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which the version was resolved.
+     * 
+     * @param repository The repository from which the version was resolved, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     */
+    public VersionResult setRepository( ArtifactRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getVersion() + " @ " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/package-info.java
new file mode 100644
index 0000000..33f9ef8
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The types supporting the resolution of artifacts and metadata from repositories.
+ */
+package org.eclipse.aether.resolution;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/AbstractTransferListener.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/AbstractTransferListener.java
new file mode 100644
index 0000000..5691e31
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/AbstractTransferListener.java
@@ -0,0 +1,64 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+/**
+ * A skeleton implementation for custom transfer listeners. The callback methods in this class do nothing.
+ */
+public abstract class AbstractTransferListener
+    implements TransferListener
+{
+
+    /**
+     * Enables subclassing.
+     */
+    protected AbstractTransferListener()
+    {
+    }
+
+    public void transferInitiated( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferStarted( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferProgressed( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException
+    {
+    }
+
+    public void transferSucceeded( TransferEvent event )
+    {
+    }
+
+    public void transferFailed( TransferEvent event )
+    {
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactNotFoundException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactNotFoundException.java
new file mode 100644
index 0000000..89a50d4
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactNotFoundException.java
@@ -0,0 +1,104 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when an artifact was not found in a particular repository.
+ */
+public class ArtifactNotFoundException
+    extends ArtifactTransferException
+{
+
+    /**
+     * Creates a new exception with the specified artifact and repository.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository )
+    {
+        super( artifact, repository, getMessage( artifact, repository ) );
+    }
+
+    private static String getMessage( Artifact artifact, RemoteRepository repository )
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        buffer.append( "Could not find artifact " ).append( artifact );
+        buffer.append( getString( " in ", repository ) );
+        if ( artifact != null )
+        {
+            String localPath = artifact.getProperty( ArtifactProperties.LOCAL_PATH, null );
+            if ( localPath != null && repository == null )
+            {
+                buffer.append( " at specified path " ).append( localPath );
+            }
+            String downloadUrl = artifact.getProperty( ArtifactProperties.DOWNLOAD_URL, null );
+            if ( downloadUrl != null )
+            {
+                buffer.append( ", try downloading from " ).append( downloadUrl );
+            }
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message )
+    {
+        super( artifact, repository, message );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( artifact, repository, message, fromCache );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository, detail message and cause.
+     * 
+     * @param artifact The missing artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactNotFoundException( Artifact artifact, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( artifact, repository, message, cause );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactTransferException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactTransferException.java
new file mode 100644
index 0000000..087040f
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactTransferException.java
@@ -0,0 +1,140 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when an artifact could not be uploaded/downloaded to/from a particular remote repository.
+ */
+public class ArtifactTransferException
+    extends RepositoryException
+{
+
+    private final transient Artifact artifact;
+
+    private final transient RemoteRepository repository;
+
+    private final boolean fromCache;
+
+    static String getString( String prefix, RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getUrl() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message )
+    {
+        this( artifact, repository, message, false );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and detail message.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( message );
+        this.artifact = artifact;
+        this.repository = repository;
+        this.fromCache = fromCache;
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository and cause.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, Throwable cause )
+    {
+        this( artifact, repository, "Could not transfer artifact " + artifact + getString( " from/to ", repository )
+            + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified artifact, repository, detail message and cause.
+     * 
+     * @param artifact The untransferable artifact, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ArtifactTransferException( Artifact artifact, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.artifact = artifact;
+        this.repository = repository;
+        this.fromCache = false;
+    }
+
+    /**
+     * Gets the artifact that could not be transferred.
+     * 
+     * @return The troublesome artifact or {@code null} if unknown.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Gets the remote repository involved in the transfer.
+     * 
+     * @return The involved remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Indicates whether this exception actually just occurred or was played back from the error cache.
+     * 
+     * @return {@code true} if the exception was played back from the error cache, {@code false} if the exception
+     *         actually occurred just now.
+     */
+    public boolean isFromCache()
+    {
+        return fromCache;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java
new file mode 100644
index 0000000..1dbc6b0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java
@@ -0,0 +1,131 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case of a checksum failure during an artifact/metadata download.
+ */
+public class ChecksumFailureException
+    extends RepositoryException
+{
+
+    private final String expected;
+
+    private final String actual;
+
+    private final boolean retryWorthy;
+
+    /**
+     * Creates a new exception with the specified expected and actual checksum. The resulting exception is
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param expected The expected checksum as declared by the hosting repository, may be {@code null}.
+     * @param actual The actual checksum as computed from the local bytes, may be {@code null}.
+     */
+    public ChecksumFailureException( String expected, String actual )
+    {
+        super( "Checksum validation failed, expected " + expected + " but is " + actual );
+        this.expected = expected;
+        this.actual = actual;
+        retryWorthy = true;
+    }
+
+    /**
+     * Creates a new exception with the specified detail message. The resulting exception is not
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public ChecksumFailureException( String message )
+    {
+        this( false, message, null );
+    }
+
+    /**
+     * Creates a new exception with the specified cause. The resulting exception is not {@link #isRetryWorthy()
+     * retry-worthy}.
+     * 
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( Throwable cause )
+    {
+        this( "Checksum validation failed" + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause. The resulting exception is not
+     * {@link #isRetryWorthy() retry-worthy}.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( String message, Throwable cause )
+    {
+        this( false, message, cause );
+    }
+
+    /**
+     * Creates a new exception with the specified retry flag, detail message and cause.
+     * 
+     * @param retryWorthy {@code true} if the exception is retry-worthy, {@code false} otherwise.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public ChecksumFailureException( boolean retryWorthy, String message, Throwable cause )
+    {
+        super( message, cause );
+        expected = actual = "";
+        this.retryWorthy = retryWorthy;
+    }
+
+    /**
+     * Gets the expected checksum for the downloaded artifact/metadata.
+     * 
+     * @return The expected checksum as declared by the hosting repository or {@code null} if unknown.
+     */
+    public String getExpected()
+    {
+        return expected;
+    }
+
+    /**
+     * Gets the actual checksum for the downloaded artifact/metadata.
+     * 
+     * @return The actual checksum as computed from the local bytes or {@code null} if unknown.
+     */
+    public String getActual()
+    {
+        return actual;
+    }
+
+    /**
+     * Indicates whether the corresponding download is retry-worthy.
+     * 
+     * @return {@code true} if retrying the download might solve the checksum failure, {@code false} if the checksum
+     *         failure is non-recoverable.
+     */
+    public boolean isRetryWorthy()
+    {
+        return retryWorthy;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java
new file mode 100644
index 0000000..9642621
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when metadata was not found in a particular repository.
+ */
+public class MetadataNotFoundException
+    extends MetadataTransferException
+{
+
+    /**
+     * Creates a new exception with the specified metadata and local repository.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved local repository, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, LocalRepository repository )
+    {
+        super( metadata, null, "Could not find metadata " + metadata + getString( " in ", repository ) );
+    }
+
+    private static String getString( String prefix, LocalRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getBasedir() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified metadata and repository.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository )
+    {
+        super( metadata, repository, "Could not find metadata " + metadata + getString( " in ", repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message )
+    {
+        super( metadata, repository, message );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( metadata, repository, message, fromCache );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository, detail message and cause.
+     * 
+     * @param metadata The missing metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataNotFoundException( Metadata metadata, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( metadata, repository, message, cause );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataTransferException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataTransferException.java
new file mode 100644
index 0000000..df6374c
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataTransferException.java
@@ -0,0 +1,140 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when metadata could not be uploaded/downloaded to/from a particular remote repository.
+ */
+public class MetadataTransferException
+    extends RepositoryException
+{
+
+    private final transient Metadata metadata;
+
+    private final transient RemoteRepository repository;
+
+    private final boolean fromCache;
+
+    static String getString( String prefix, RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "";
+        }
+        else
+        {
+            return prefix + repository.getId() + " (" + repository.getUrl() + ")";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message )
+    {
+        this( metadata, repository, message, false );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and detail message.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param fromCache {@code true} if the exception was played back from the error cache, {@code false} if the
+     *            exception actually just occurred.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message, boolean fromCache )
+    {
+        super( message );
+        this.metadata = metadata;
+        this.repository = repository;
+        this.fromCache = fromCache;
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository and cause.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, Throwable cause )
+    {
+        this( metadata, repository, "Could not transfer metadata " + metadata + getString( " from/to ", repository )
+            + getMessage( ": ", cause ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified metadata, repository, detail message and cause.
+     * 
+     * @param metadata The untransferable metadata, may be {@code null}.
+     * @param repository The involved remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public MetadataTransferException( Metadata metadata, RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.metadata = metadata;
+        this.repository = repository;
+        this.fromCache = false;
+    }
+
+    /**
+     * Gets the metadata that could not be transferred.
+     * 
+     * @return The troublesome metadata or {@code null} if unknown.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Gets the remote repository involved in the transfer.
+     * 
+     * @return The involved remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Indicates whether this exception actually just occurred or was played back from the error cache.
+     * 
+     * @return {@code true} if the exception was played back from the error cache, {@code false} if the exception
+     *         actually occurred just now.
+     */
+    public boolean isFromCache()
+    {
+        return fromCache;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryConnectorException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryConnectorException.java
new file mode 100644
index 0000000..3140569
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryConnectorException.java
@@ -0,0 +1,103 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported remote repository type.
+ */
+public class NoRepositoryConnectorException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose content type is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryConnectorException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "No connector available to access repository " + repository.getId() + " (" + repository.getUrl()
+                + ") of type " + repository.getContentType();
+        }
+        else
+        {
+            return "No connector available to access repository";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose content type is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryLayoutException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryLayoutException.java
new file mode 100644
index 0000000..3fc05bb
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoRepositoryLayoutException.java
@@ -0,0 +1,102 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported repository layout.
+ */
+public class NoRepositoryLayoutException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoRepositoryLayoutException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "Unsupported repository layout " + repository.getContentType();
+        }
+        else
+        {
+            return "Unsupported repository layout";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose layout is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoTransporterException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoTransporterException.java
new file mode 100644
index 0000000..5d98558
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/NoTransporterException.java
@@ -0,0 +1,102 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown in case of an unsupported transport protocol.
+ */
+public class NoTransporterException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository )
+    {
+        this( repository, toMessage( repository ) );
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and cause.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, Throwable cause )
+    {
+        this( repository, toMessage( repository ), cause );
+    }
+
+    /**
+     * Creates a new exception with the specified repository, detail message and cause.
+     * 
+     * @param repository The remote repository whose transport layout is not supported, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public NoTransporterException( RemoteRepository repository, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.repository = repository;
+    }
+
+    private static String toMessage( RemoteRepository repository )
+    {
+        if ( repository != null )
+        {
+            return "Unsupported transport protocol " + repository.getProtocol();
+        }
+        else
+        {
+            return "Unsupported transport protocol";
+        }
+    }
+
+    /**
+     * Gets the remote repository whose transport protocol is not supported.
+     * 
+     * @return The unsupported remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/RepositoryOfflineException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/RepositoryOfflineException.java
new file mode 100644
index 0000000..02d4680
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/RepositoryOfflineException.java
@@ -0,0 +1,79 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Thrown when a transfer could not be performed because a remote repository is not accessible in offline mode.
+ */
+public class RepositoryOfflineException
+    extends RepositoryException
+{
+
+    private final transient RemoteRepository repository;
+
+    private static String getMessage( RemoteRepository repository )
+    {
+        if ( repository == null )
+        {
+            return "Cannot access remote repositories in offline mode";
+        }
+        else
+        {
+            return "Cannot access " + repository.getId() + " (" + repository.getUrl() + ") in offline mode";
+        }
+    }
+
+    /**
+     * Creates a new exception with the specified repository.
+     * 
+     * @param repository The inaccessible remote repository, may be {@code null}.
+     */
+    public RepositoryOfflineException( RemoteRepository repository )
+    {
+        super( getMessage( repository ) );
+        this.repository = repository;
+    }
+
+    /**
+     * Creates a new exception with the specified repository and detail message.
+     * 
+     * @param repository The inaccessible remote repository, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public RepositoryOfflineException( RemoteRepository repository, String message )
+    {
+        super( message );
+        this.repository = repository;
+    }
+
+    /**
+     * Gets the remote repository that could not be accessed due to offline mode.
+     * 
+     * @return The inaccessible remote repository or {@code null} if unknown.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferCancelledException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferCancelledException.java
new file mode 100644
index 0000000..88caa13
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferCancelledException.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown in case an upload/download was cancelled (e.g. due to user request).
+ */
+public class TransferCancelledException
+    extends RepositoryException
+{
+
+    /**
+     * Creates a new exception with a stock detail message.
+     */
+    public TransferCancelledException()
+    {
+        super( "The operation was cancelled." );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message.
+     * 
+     * @param message The detail message, may be {@code null}.
+     */
+    public TransferCancelledException( String message )
+    {
+        super( message );
+    }
+
+    /**
+     * Creates a new exception with the specified detail message and cause.
+     * 
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public TransferCancelledException( String message, Throwable cause )
+    {
+        super( message, cause );
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferEvent.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferEvent.java
new file mode 100644
index 0000000..7d33d50
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferEvent.java
@@ -0,0 +1,413 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import java.nio.ByteBuffer;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * An event fired to a transfer listener during an artifact/metadata transfer.
+ *
+ * @see TransferListener
+ * @see TransferEvent.Builder
+ */
+public final class TransferEvent
+{
+
+    /**
+     * The type of the event.
+     */
+    public enum EventType
+    {
+
+        /**
+         * @see TransferListener#transferInitiated(TransferEvent)
+         */
+        INITIATED,
+
+        /**
+         * @see TransferListener#transferStarted(TransferEvent)
+         */
+        STARTED,
+
+        /**
+         * @see TransferListener#transferProgressed(TransferEvent)
+         */
+        PROGRESSED,
+
+        /**
+         * @see TransferListener#transferCorrupted(TransferEvent)
+         */
+        CORRUPTED,
+
+        /**
+         * @see TransferListener#transferSucceeded(TransferEvent)
+         */
+        SUCCEEDED,
+
+        /**
+         * @see TransferListener#transferFailed(TransferEvent)
+         */
+        FAILED
+
+    }
+
+    /**
+     * The type of the request/transfer being performed.
+     */
+    public enum RequestType
+    {
+
+        /**
+         * Download artifact/metadata.
+         */
+        GET,
+
+        /**
+         * Check artifact/metadata existence only.
+         */
+        GET_EXISTENCE,
+
+        /**
+         * Upload artifact/metadata.
+         */
+        PUT,
+
+    }
+
+    private final EventType type;
+
+    private final RequestType requestType;
+
+    private final RepositorySystemSession session;
+
+    private final TransferResource resource;
+
+    private final ByteBuffer dataBuffer;
+
+    private final long transferredBytes;
+
+    private final Exception exception;
+
+    TransferEvent( Builder builder )
+    {
+        type = builder.type;
+        requestType = builder.requestType;
+        session = builder.session;
+        resource = builder.resource;
+        dataBuffer = builder.dataBuffer;
+        transferredBytes = builder.transferredBytes;
+        exception = builder.exception;
+    }
+
+    /**
+     * Gets the type of the event.
+     * 
+     * @return The type of the event, never {@code null}.
+     */
+    public EventType getType()
+    {
+        return type;
+    }
+
+    /**
+     * Gets the type of the request/transfer.
+     * 
+     * @return The type of the request/transfer, never {@code null}.
+     */
+    public RequestType getRequestType()
+    {
+        return requestType;
+    }
+
+    /**
+     * Gets the repository system session during which the event occurred.
+     * 
+     * @return The repository system session during which the event occurred, never {@code null}.
+     */
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    /**
+     * Gets the resource that is being transferred.
+     * 
+     * @return The resource being transferred, never {@code null}.
+     */
+    public TransferResource getResource()
+    {
+        return resource;
+    }
+
+    /**
+     * Gets the total number of bytes that have been transferred since the download/upload of the resource was started.
+     * If a download has been resumed, the returned count includes the bytes that were already downloaded during the
+     * previous attempt. In other words, the ratio of transferred bytes to the content length of the resource indicates
+     * the percentage of transfer completion.
+     * 
+     * @return The total number of bytes that have been transferred since the transfer started, never negative.
+     * @see #getDataLength()
+     * @see TransferResource#getResumeOffset()
+     */
+    public long getTransferredBytes()
+    {
+        return transferredBytes;
+    }
+
+    /**
+     * Gets the byte buffer holding the transferred bytes since the last event. A listener must assume this buffer to be
+     * owned by the event source and must not change any byte in this buffer. Also, the buffer is only valid for the
+     * duration of the event callback, i.e. the next event might reuse the same buffer (with updated contents).
+     * Therefore, if the actual event processing is deferred, the byte buffer would have to be cloned to create an
+     * immutable snapshot of its contents.
+     * 
+     * @return The (read-only) byte buffer or {@code null} if not applicable to the event, i.e. if the event type is not
+     *         {@link EventType#PROGRESSED}.
+     */
+    public ByteBuffer getDataBuffer()
+    {
+        return ( dataBuffer != null ) ? dataBuffer.asReadOnlyBuffer() : null;
+    }
+
+    /**
+     * Gets the number of bytes that have been transferred since the last event.
+     * 
+     * @return The number of bytes that have been transferred since the last event, possibly zero but never negative.
+     * @see #getTransferredBytes()
+     */
+    public int getDataLength()
+    {
+        return ( dataBuffer != null ) ? dataBuffer.remaining() : 0;
+    }
+
+    /**
+     * Gets the error that occurred during the transfer.
+     * 
+     * @return The error that occurred or {@code null} if none.
+     */
+    public Exception getException()
+    {
+        return exception;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRequestType() + " " + getType() + " " + getResource();
+    }
+
+    /**
+     * A builder to create transfer events.
+     */
+    public static final class Builder
+    {
+
+        EventType type;
+
+        RequestType requestType;
+
+        RepositorySystemSession session;
+
+        TransferResource resource;
+
+        ByteBuffer dataBuffer;
+
+        long transferredBytes;
+
+        Exception exception;
+
+        /**
+         * Creates a new transfer event builder for the specified session and the given resource.
+         *
+         * @param session The repository system session, must not be {@code null}.
+         * @param resource The resource being transferred, must not be {@code null}.
+         */
+        public Builder( RepositorySystemSession session, TransferResource resource )
+        {
+            this.session = requireNonNull( session, "repository system session cannot be null" );
+            this.resource = requireNonNull( resource, "transfer resource cannot be null" );
+            type = EventType.INITIATED;
+            requestType = RequestType.GET;
+        }
+
+        private Builder( Builder prototype )
+        {
+            session = prototype.session;
+            resource = prototype.resource;
+            type = prototype.type;
+            requestType = prototype.requestType;
+            dataBuffer = prototype.dataBuffer;
+            transferredBytes = prototype.transferredBytes;
+            exception = prototype.exception;
+        }
+
+        /**
+         * Creates a new transfer event builder from the current values of this builder. The state of this builder
+         * remains unchanged.
+         * 
+         * @return The new event builder, never {@code null}.
+         */
+        public Builder copy()
+        {
+            return new Builder( this );
+        }
+
+        /**
+         * Sets the type of the event and resets event-specific fields. In more detail, the data buffer and the
+         * exception fields are set to {@code null}. Furthermore, the total number of transferred bytes is set to
+         * {@code 0} if the event type is {@link EventType#STARTED}.
+         *
+         * @param type The type of the event, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder resetType( EventType type )
+        {
+            this.type = requireNonNull( type, "event type cannot be null" );
+            dataBuffer = null;
+            exception = null;
+            switch ( type )
+            {
+                case INITIATED:
+                case STARTED:
+                    transferredBytes = 0L;
+                default:
+            }
+            return this;
+        }
+
+        /**
+         * Sets the type of the event. When re-using the same builder to generate a sequence of events for one transfer,
+         * {@link #resetType(TransferEvent.EventType)} might be more handy.
+         *
+         * @param type The type of the event, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setType( EventType type )
+        {
+            this.type = requireNonNull( type, "event type cannot be null" );
+            return this;
+        }
+
+        /**
+         * Sets the type of the request/transfer.
+         *
+         * @param requestType The request/transfer type, must not be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setRequestType( RequestType requestType )
+        {
+            this.requestType = requireNonNull( requestType, "request type cannot be null" );
+            return this;
+        }
+
+        /**
+         * Sets the total number of bytes that have been transferred so far during the download/upload of the resource.
+         * If a download is being resumed, the count must include the bytes that were already downloaded in the previous
+         * attempt and from which the current transfer started. In this case, the event type {@link EventType#STARTED}
+         * should indicate from what byte the download resumes.
+         * 
+         * @param transferredBytes The total number of bytes that have been transferred so far during the
+         *            download/upload of the resource, must not be negative.
+         * @return This event builder for chaining, never {@code null}.
+         * @see TransferResource#setResumeOffset(long)
+         */
+        public Builder setTransferredBytes( long transferredBytes )
+        {
+            if ( transferredBytes < 0L )
+            {
+                throw new IllegalArgumentException( "number of transferred bytes cannot be negative" );
+            }
+            this.transferredBytes = transferredBytes;
+            return this;
+        }
+
+        /**
+         * Increments the total number of bytes that have been transferred so far during the download/upload.
+         *
+         * @param transferredBytes The number of bytes that have been transferred since the last event, must not be
+         *            negative.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder addTransferredBytes( long transferredBytes )
+        {
+            if ( transferredBytes < 0L )
+            {
+                throw new IllegalArgumentException( "number of transferred bytes cannot be negative" );
+            }
+            this.transferredBytes += transferredBytes;
+            return this;
+        }
+
+        /**
+         * Sets the byte buffer holding the transferred bytes since the last event.
+         * 
+         * @param buffer The byte buffer holding the transferred bytes since the last event, may be {@code null} if not
+         *            applicable to the event.
+         * @param offset The starting point of valid bytes in the array.
+         * @param length The number of valid bytes, must not be negative.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setDataBuffer( byte[] buffer, int offset, int length )
+        {
+            return setDataBuffer( ( buffer != null ) ? ByteBuffer.wrap( buffer, offset, length ) : null );
+        }
+
+        /**
+         * Sets the byte buffer holding the transferred bytes since the last event.
+         * 
+         * @param dataBuffer The byte buffer holding the transferred bytes since the last event, may be {@code null} if
+         *            not applicable to the event.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setDataBuffer( ByteBuffer dataBuffer )
+        {
+            this.dataBuffer = dataBuffer;
+            return this;
+        }
+
+        /**
+         * Sets the error that occurred during the transfer.
+         * 
+         * @param exception The error that occurred during the transfer, may be {@code null} if none.
+         * @return This event builder for chaining, never {@code null}.
+         */
+        public Builder setException( Exception exception )
+        {
+            this.exception = exception;
+            return this;
+        }
+
+        /**
+         * Builds a new transfer event from the current values of this builder. The state of the builder itself remains
+         * unchanged.
+         * 
+         * @return The transfer event, never {@code null}.
+         */
+        public TransferEvent build()
+        {
+            return new TransferEvent( this );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferListener.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferListener.java
new file mode 100644
index 0000000..18019a9
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferListener.java
@@ -0,0 +1,100 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+/**
+ * A listener being notified of artifact/metadata transfers from/to remote repositories. The listener may be called from
+ * an arbitrary thread. Reusing common regular expression syntax, the sequence of events is roughly as follows:
+ * 
+ * <pre>
+ * INITIATED ( STARTED PROGRESSED* CORRUPTED? )* ( SUCCEEDED | FAILED )
+ * </pre>
+ * 
+ * <em>Note:</em> Implementors are strongly advised to inherit from {@link AbstractTransferListener} instead of directly
+ * implementing this interface.
+ * 
+ * @see org.eclipse.aether.RepositorySystemSession#getTransferListener()
+ * @see org.eclipse.aether.RepositoryListener
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface TransferListener
+{
+
+    /**
+     * Notifies the listener about the initiation of a transfer. This event gets fired before any actual network access
+     * to the remote repository and usually indicates some thread is now about to perform the transfer. For a given
+     * transfer request, this event is the first one being fired and it must be emitted exactly once.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferInitiated( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about the start of a data transfer. This event indicates a successful connection to the
+     * remote repository. In case of a download, the requested remote resource exists and its size is given by
+     * {@link TransferResource#getContentLength()} if possible. This event may be fired multiple times for given
+     * transfer request if said transfer needs to be repeated (e.g. in response to an authentication challenge).
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferStarted( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about some progress in the data transfer. This event may even be fired if actually zero
+     * bytes have been transferred since the last event, for instance to enable cancellation.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferProgressed( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener that a checksum validation failed. {@link TransferEvent#getException()} will be of type
+     * {@link ChecksumFailureException} and can be used to query further details about the expected/actual checksums.
+     * 
+     * @param event The event details, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException;
+
+    /**
+     * Notifies the listener about the successful completion of a transfer. This event must be fired exactly once for a
+     * given transfer request unless said request failed.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void transferSucceeded( TransferEvent event );
+
+    /**
+     * Notifies the listener about the unsuccessful termination of a transfer. {@link TransferEvent#getException()} will
+     * provide further information about the failure.
+     * 
+     * @param event The event details, must not be {@code null}.
+     */
+    void transferFailed( TransferEvent event );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java
new file mode 100644
index 0000000..26b6c77
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java
@@ -0,0 +1,247 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.RequestTrace;
+
+/**
+ * Describes a resource being uploaded or downloaded by the repository system.
+ */
+public final class TransferResource
+{
+
+    private final String repositoryId;
+
+    private final String repositoryUrl;
+
+    private final String resourceName;
+
+    private final File file;
+
+    private final long startTime;
+
+    private final RequestTrace trace;
+
+    private long contentLength = -1L;
+
+    private long resumeOffset;
+
+    /**
+     * Creates a new transfer resource with the specified properties.
+     *
+     * @param repositoryUrl The base URL of the repository, may be {@code null} or empty if unknown. If not empty, a
+     * trailing slash will automatically be added if missing.
+     * @param resourceName The relative path to the resource within the repository, may be {@code null}. A leading slash
+     * (if any) will be automatically removed.
+     * @param file The source/target file involved in the transfer, may be {@code null}.
+     * @param trace The trace information, may be {@code null}.
+     *
+     * @deprecated As of 1.1.0, replaced by {@link #TransferResource(java.lang.String, java.lang.String,
+     * java.lang.String, java.io.File, org.eclipse.aether.RequestTrace)}
+     */
+    @Deprecated
+    public TransferResource( String repositoryUrl, String resourceName, File file, RequestTrace trace )
+    {
+        this( null, repositoryUrl, resourceName, file, trace );
+    }
+
+    /**
+     * Creates a new transfer resource with the specified properties.
+     *
+     * @param repositoryId The ID of the repository used to transfer the resource, may be {@code null} or empty if unknown.
+     * @param repositoryUrl The base URL of the repository, may be {@code null} or empty if unknown. If not empty, a
+     *            trailing slash will automatically be added if missing.
+     * @param resourceName The relative path to the resource within the repository, may be {@code null}. A leading slash
+     *            (if any) will be automatically removed.
+     * @param file The source/target file involved in the transfer, may be {@code null}.
+     * @param trace The trace information, may be {@code null}.
+     *
+     * @since 1.1.0
+     */
+    public TransferResource( String repositoryId, String repositoryUrl, String resourceName,
+        File file, RequestTrace trace )
+    {
+        if ( repositoryId == null || repositoryId.length() <= 0 )
+        {
+            this.repositoryId = "";
+        }
+        else
+        {
+            this.repositoryId = repositoryId;
+        }
+
+        if ( repositoryUrl == null || repositoryUrl.length() <= 0 )
+        {
+            this.repositoryUrl = "";
+        }
+        else if ( repositoryUrl.endsWith( "/" ) )
+        {
+            this.repositoryUrl = repositoryUrl;
+        }
+        else
+        {
+            this.repositoryUrl = repositoryUrl + '/';
+        }
+
+        if ( resourceName == null || resourceName.length() <= 0 )
+        {
+            this.resourceName = "";
+        }
+        else if ( resourceName.startsWith( "/" ) )
+        {
+            this.resourceName = resourceName.substring( 1 );
+        }
+        else
+        {
+            this.resourceName = resourceName;
+        }
+
+        this.file = file;
+
+        this.trace = trace;
+
+        startTime = System.currentTimeMillis();
+    }
+
+    /**
+     * The ID of the repository, e.g., "central".
+     *
+     * @return The ID of the repository or an empty string if unknown, never {@code null}.
+     *
+     * @since 1.1.0
+     */
+    public String getRepositoryId()
+    {
+        return repositoryId;
+    }
+
+    /**
+     * The base URL of the repository, e.g. "http://repo1.maven.org/maven2/". Unless the URL is unknown, it will be
+     * terminated by a trailing slash.
+     *
+     * @return The base URL of the repository or an empty string if unknown, never {@code null}.
+     */
+    public String getRepositoryUrl()
+    {
+        return repositoryUrl;
+    }
+
+    /**
+     * The path of the resource relative to the repository's base URL, e.g. "org/apache/maven/maven/3.0/maven-3.0.pom".
+     *
+     * @return The path of the resource, never {@code null}.
+     */
+    public String getResourceName()
+    {
+        return resourceName;
+    }
+
+    /**
+     * Gets the local file being uploaded or downloaded. When the repository system merely checks for the existence of a
+     * remote resource, no local file will be involved in the transfer.
+     *
+     * @return The source/target file involved in the transfer or {@code null} if none.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * The size of the resource in bytes. Note that the size of a resource during downloads might be unknown to the
+     * client which is usually the case when transfers employ compression like gzip. In general, the content length is
+     * not known until the transfer has {@link TransferListener#transferStarted(TransferEvent) started}.
+     *
+     * @return The size of the resource in bytes or a negative value if unknown.
+     */
+    public long getContentLength()
+    {
+        return contentLength;
+    }
+
+    /**
+     * Sets the size of the resource in bytes.
+     *
+     * @param contentLength The size of the resource in bytes or a negative value if unknown.
+     * @return This resource for chaining, never {@code null}.
+     */
+    public TransferResource setContentLength( long contentLength )
+    {
+        this.contentLength = contentLength;
+        return this;
+    }
+
+    /**
+     * Gets the byte offset within the resource from which the download starts. A positive offset indicates a previous
+     * download attempt is being resumed, {@code 0} means the transfer starts at the first byte.
+     *
+     * @return The zero-based index of the first byte being transferred, never negative.
+     */
+    public long getResumeOffset()
+    {
+        return resumeOffset;
+    }
+
+    /**
+     * Sets the byte offset within the resource at which the download starts.
+     *
+     * @param resumeOffset The zero-based index of the first byte being transferred, must not be negative.
+     * @return This resource for chaining, never {@code null}.
+     */
+    public TransferResource setResumeOffset( long resumeOffset )
+    {
+        if ( resumeOffset < 0L )
+        {
+            throw new IllegalArgumentException( "resume offset cannot be negative" );
+        }
+        this.resumeOffset = resumeOffset;
+        return this;
+    }
+
+    /**
+     * Gets the timestamp when the transfer of this resource was started.
+     *
+     * @return The timestamp when the transfer of this resource was started.
+     */
+    public long getTransferStartTime()
+    {
+        return startTime;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation during which this resource is
+     * transferred.
+     *
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getRepositoryUrl() + getResourceName() + " <> " + getFile();
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/package-info.java
new file mode 100644
index 0000000..541b244
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/package-info.java
@@ -0,0 +1,25 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * A listener and various exception types dealing with the transfer of a resource between the local system and a remote
+ * repository.
+ */
+package org.eclipse.aether.transfer;
+
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/InvalidVersionSpecificationException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/InvalidVersionSpecificationException.java
new file mode 100644
index 0000000..a576844
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/InvalidVersionSpecificationException.java
@@ -0,0 +1,80 @@
+package org.eclipse.aether.version;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+
+/**
+ * Thrown when a version or version range could not be parsed.
+ */
+public class InvalidVersionSpecificationException
+    extends RepositoryException
+{
+
+    private final String version;
+
+    /**
+     * Creates a new exception with the specified version and detail message.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, String message )
+    {
+        super( message );
+        this.version = version;
+    }
+
+    /**
+     * Creates a new exception with the specified version and cause.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, Throwable cause )
+    {
+        super( "Could not parse version specification " + version + getMessage( ": ", cause ), cause );
+        this.version = version;
+    }
+
+    /**
+     * Creates a new exception with the specified version, detail message and cause.
+     * 
+     * @param version The invalid version specification, may be {@code null}.
+     * @param message The detail message, may be {@code null}.
+     * @param cause The exception that caused this one, may be {@code null}.
+     */
+    public InvalidVersionSpecificationException( String version, String message, Throwable cause )
+    {
+        super( message, cause );
+        this.version = version;
+    }
+
+    /**
+     * Gets the version or version range that could not be parsed.
+     * 
+     * @return The invalid version specification or {@code null} if unknown.
+     */
+    public String getVersion()
+    {
+        return version;
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/Version.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/Version.java
new file mode 100644
index 0000000..41c02c0
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/Version.java
@@ -0,0 +1,36 @@
+package org.eclipse.aether.version;
+
+/*
+ * 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.
+ */
+
+/**
+ * A parsed artifact version.
+ */
+public interface Version
+    extends Comparable<Version>
+{
+
+    /**
+     * Gets the original string representation of the version.
+     * 
+     * @return The string representation of the version, never {@code null}.
+     */
+    String toString();
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionConstraint.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionConstraint.java
new file mode 100644
index 0000000..1c68587
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionConstraint.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.version;
+
+/*
+ * 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.
+ */
+
+/**
+ * A constraint on versions for a dependency. A constraint can either consist of a version range (e.g. "[1, ]") or a
+ * single version (e.g. "1.1"). In the first case, the constraint expresses a hard requirement on a version matching the
+ * range. In the second case, the constraint expresses a soft requirement on a specific version (i.e. a recommendation).
+ */
+public interface VersionConstraint
+{
+
+    /**
+     * Gets the version range of this constraint.
+     * 
+     * @return The version range or {@code null} if none.
+     */
+    VersionRange getRange();
+
+    /**
+     * Gets the version recommended by this constraint.
+     * 
+     * @return The recommended version or {@code null} if none.
+     */
+    Version getVersion();
+
+    /**
+     * Determines whether the specified version satisfies this constraint. In more detail, a version satisfies this
+     * constraint if it matches its version range or if this constraint has no version range and the specified version
+     * equals the version recommended by the constraint.
+     * 
+     * @param version The version to test, must not be {@code null}.
+     * @return {@code true} if the specified version satisfies this constraint, {@code false} otherwise.
+     */
+    boolean containsVersion( Version version );
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionRange.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionRange.java
new file mode 100644
index 0000000..fbbb808
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionRange.java
@@ -0,0 +1,129 @@
+package org.eclipse.aether.version;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A range of versions.
+ */
+public interface VersionRange
+{
+
+    /**
+     * Determines whether the specified version is contained within this range.
+     * 
+     * @param version The version to test, must not be {@code null}.
+     * @return {@code true} if this range contains the specified version, {@code false} otherwise.
+     */
+    boolean containsVersion( Version version );
+
+    /**
+     * Gets a lower bound (if any) for this range. If existent, this range does not contain any version smaller than its
+     * lower bound. Note that complex version ranges might exclude some versions even within their bounds.
+     * 
+     * @return A lower bound for this range or {@code null} is there is none.
+     */
+    Bound getLowerBound();
+
+    /**
+     * Gets an upper bound (if any) for this range. If existent, this range does not contain any version greater than
+     * its upper bound. Note that complex version ranges might exclude some versions even within their bounds.
+     * 
+     * @return An upper bound for this range or {@code null} is there is none.
+     */
+    Bound getUpperBound();
+
+    /**
+     * A bound of a version range.
+     */
+    static final class Bound
+    {
+
+        private final Version version;
+
+        private final boolean inclusive;
+
+        /**
+         * Creates a new bound with the specified properties.
+         *
+         * @param version The bounding version, must not be {@code null}.
+         * @param inclusive A flag whether the specified version is included in the range or not.
+         */
+        public Bound( Version version, boolean inclusive )
+        {
+            this.version = requireNonNull( version, "version cannot be null" );
+            this.inclusive = inclusive;
+        }
+
+        /**
+         * Gets the bounding version.
+         *
+         * @return The bounding version, never {@code null}.
+         */
+        public Version getVersion()
+        {
+            return version;
+        }
+
+        /**
+         * Indicates whether the bounding version is included in the range or not.
+         * 
+         * @return {@code true} if the bounding version is included in the range, {@code false} if not.
+         */
+        public boolean isInclusive()
+        {
+            return inclusive;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( obj == null || !getClass().equals( obj.getClass() ) )
+            {
+                return false;
+            }
+
+            Bound that = (Bound) obj;
+            return inclusive == that.inclusive && version.equals( that.version );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            int hash = 17;
+            hash = hash * 31 + version.hashCode();
+            hash = hash * 31 + ( inclusive ? 1 : 0 );
+            return hash;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( version );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionScheme.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionScheme.java
new file mode 100644
index 0000000..c765a03
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/VersionScheme.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether.version;
+
+/*
+ * 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.
+ */
+
+/**
+ * A version scheme that handles interpretation of version strings to facilitate their comparison.
+ */
+public interface VersionScheme
+{
+
+    /**
+     * Parses the specified version string, for example "1.0".
+     * 
+     * @param version The version string to parse, must not be {@code null}.
+     * @return The parsed version, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the string violates the syntax rules of this scheme.
+     */
+    Version parseVersion( String version )
+        throws InvalidVersionSpecificationException;
+
+    /**
+     * Parses the specified version range specification, for example "[1.0,2.0)".
+     * 
+     * @param range The range specification to parse, must not be {@code null}.
+     * @return The parsed version range, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the range specification violates the syntax rules of this scheme.
+     */
+    VersionRange parseVersionRange( String range )
+        throws InvalidVersionSpecificationException;
+
+    /**
+     * Parses the specified version constraint specification, for example "1.0" or "[1.0,2.0),(2.0,)".
+     * 
+     * @param constraint The constraint specification to parse, must not be {@code null}.
+     * @return The parsed version constraint, never {@code null}.
+     * @throws InvalidVersionSpecificationException If the constraint specification violates the syntax rules of this
+     *             scheme.
+     */
+    VersionConstraint parseVersionConstraint( final String constraint )
+        throws InvalidVersionSpecificationException;
+
+}
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/version/package-info.java b/maven-resolver-api/src/main/java/org/eclipse/aether/version/package-info.java
new file mode 100644
index 0000000..a16dd64
--- /dev/null
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/version/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The definition of a version scheme for parsing and comparing versions.
+ */
+package org.eclipse.aether.version;
+
diff --git a/maven-resolver-api/src/site/site.xml b/maven-resolver-api/src/site/site.xml
new file mode 100644
index 0000000..033a8c4
--- /dev/null
+++ b/maven-resolver-api/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="API">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractForwardingRepositorySystemSessionTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractForwardingRepositorySystemSessionTest.java
new file mode 100644
index 0000000..5ad2475
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractForwardingRepositorySystemSessionTest.java
@@ -0,0 +1,44 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class AbstractForwardingRepositorySystemSessionTest
+{
+
+    @Test
+    public void testAllMethodsImplemented()
+        throws Exception
+    {
+        for ( Method method : RepositorySystemSession.class.getMethods() )
+        {
+            Method m =
+                AbstractForwardingRepositorySystemSession.class.getDeclaredMethod( method.getName(),
+                                                                                   method.getParameterTypes() );
+            assertNotNull( method.toString(), m );
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractRepositoryListenerTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractRepositoryListenerTest.java
new file mode 100644
index 0000000..74c617f
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/AbstractRepositoryListenerTest.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.aether.AbstractRepositoryListener;
+import org.eclipse.aether.RepositoryListener;
+import org.junit.Test;
+
+/**
+ */
+public class AbstractRepositoryListenerTest
+{
+
+    @Test
+    public void testAllEventTypesHandled()
+        throws Exception
+    {
+        for ( Method method : RepositoryListener.class.getMethods() )
+        {
+            assertNotNull( AbstractRepositoryListener.class.getDeclaredMethod( method.getName(),
+                                                                               method.getParameterTypes() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositoryCacheTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositoryCacheTest.java
new file mode 100644
index 0000000..067320e
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositoryCacheTest.java
@@ -0,0 +1,112 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Test;
+
+public class DefaultRepositoryCacheTest
+{
+
+    private DefaultRepositoryCache cache = new DefaultRepositoryCache();
+
+    private RepositorySystemSession session = new DefaultRepositorySystemSession();
+
+    private Object get( Object key )
+    {
+        return cache.get( session, key );
+    }
+
+    private void put( Object key, Object value )
+    {
+        cache.put( session, key, value );
+    }
+
+    @Test( expected = RuntimeException.class )
+    public void testGet_NullKey()
+    {
+        get( null );
+    }
+
+    @Test( expected = RuntimeException.class )
+    public void testPut_NullKey()
+    {
+        put( null, "data" );
+    }
+
+    @Test
+    public void testGetPut()
+    {
+        Object key = "key";
+        assertNull( get( key ) );
+        put( key, "value" );
+        assertEquals( "value", get( key ) );
+        put( key, "changed" );
+        assertEquals( "changed", get( key ) );
+        put( key, null );
+        assertNull( get( key ) );
+    }
+
+    @Test( timeout = 10000L )
+    public void testConcurrency()
+        throws Exception
+    {
+        final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
+        Thread threads[] = new Thread[20];
+        for ( int i = 0; i < threads.length; i++ )
+        {
+            threads[i] = new Thread()
+            {
+                @Override
+                public void run()
+                {
+                    for ( int i = 0; i < 100; i++ )
+                    {
+                        String key = UUID.randomUUID().toString();
+                        try
+                        {
+                            put( key, Boolean.TRUE );
+                            assertEquals( Boolean.TRUE, get( key ) );
+                        }
+                        catch ( Throwable t )
+                        {
+                            error.compareAndSet( null, t );
+                            t.printStackTrace();
+                        }
+                    }
+                }
+            };
+        }
+        for ( Thread thread : threads )
+        {
+            thread.start();
+        }
+        for ( Thread thread : threads )
+        {
+            thread.join();
+        }
+        assertNull( String.valueOf( error.get() ), error.get() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositorySystemSessionTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositorySystemSessionTest.java
new file mode 100644
index 0000000..91afeb5
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultRepositorySystemSessionTest.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Map;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultRepositorySystemSessionTest
+{
+
+    @Test
+    public void testDefaultProxySelectorUsesExistingProxy()
+    {
+        DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+
+        RemoteRepository repo = new RemoteRepository.Builder( "id", "default", "void" ).build();
+        assertSame( null, session.getProxySelector().getProxy( repo ) );
+
+        Proxy proxy = new Proxy( "http", "localhost", 8080, null );
+        repo = new RemoteRepository.Builder( repo ).setProxy( proxy ).build();
+        assertSame( proxy, session.getProxySelector().getProxy( repo ) );
+    }
+
+    @Test
+    public void testDefaultAuthenticationSelectorUsesExistingAuth()
+    {
+        DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+
+        RemoteRepository repo = new RemoteRepository.Builder( "id", "default", "void" ).build();
+        assertSame( null, session.getAuthenticationSelector().getAuthentication( repo ) );
+
+        Authentication auth = new Authentication()
+        {
+            public void fill( AuthenticationContext context, String key, Map<String, String> data )
+            {
+            }
+
+            public void digest( AuthenticationDigest digest )
+            {
+            }
+        };
+        repo = new RemoteRepository.Builder( repo ).setAuthentication( auth ).build();
+        assertSame( auth, session.getAuthenticationSelector().getAuthentication( repo ) );
+    }
+
+    @Test
+    public void testCopyConstructorCopiesPropertiesDeep()
+    {
+        DefaultRepositorySystemSession session1 = new DefaultRepositorySystemSession();
+        session1.setUserProperties( System.getProperties() );
+        session1.setSystemProperties( System.getProperties() );
+        session1.setConfigProperties( System.getProperties() );
+
+        DefaultRepositorySystemSession session2 = new DefaultRepositorySystemSession( session1 );
+        session2.setUserProperty( "key", "test" );
+        session2.setSystemProperty( "key", "test" );
+        session2.setConfigProperty( "key", "test" );
+
+        assertEquals( null, session1.getUserProperties().get( "key" ) );
+        assertEquals( null, session1.getSystemProperties().get( "key" ) );
+        assertEquals( null, session1.getConfigProperties().get( "key" ) );
+    }
+
+    @Test
+    public void testReadOnlyProperties()
+    {
+        DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+
+        try
+        {
+            session.getUserProperties().put( "key", "test" );
+            fail( "user properties are modifiable" );
+        }
+        catch ( UnsupportedOperationException e )
+        {
+            // expected
+        }
+
+        try
+        {
+            session.getSystemProperties().put( "key", "test" );
+            fail( "system properties are modifiable" );
+        }
+        catch ( UnsupportedOperationException e )
+        {
+            // expected
+        }
+
+        try
+        {
+            session.getConfigProperties().put( "key", "test" );
+            fail( "config properties are modifiable" );
+        }
+        catch ( UnsupportedOperationException e )
+        {
+            // expected
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultSessionDataTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultSessionDataTest.java
new file mode 100644
index 0000000..3b886e5
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/DefaultSessionDataTest.java
@@ -0,0 +1,137 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Test;
+
+public class DefaultSessionDataTest
+{
+
+    private DefaultSessionData data = new DefaultSessionData();
+
+    private Object get( Object key )
+    {
+        return data.get( key );
+    }
+
+    private void set( Object key, Object value )
+    {
+        data.set( key, value );
+    }
+
+    private boolean set( Object key, Object oldValue, Object newValue )
+    {
+        return data.set( key, oldValue, newValue );
+    }
+
+    @Test( expected = RuntimeException.class )
+    public void testGet_NullKey()
+    {
+        get( null );
+    }
+
+    @Test( expected = RuntimeException.class )
+    public void testSet_NullKey()
+    {
+        set( null, "data" );
+    }
+
+    @Test
+    public void testGetSet()
+    {
+        Object key = "key";
+        assertNull( get( key ) );
+        set( key, "value" );
+        assertEquals( "value", get( key ) );
+        set( key, "changed" );
+        assertEquals( "changed", get( key ) );
+        set( key, null );
+        assertNull( get( key ) );
+    }
+
+    @Test
+    public void testGetSafeSet()
+    {
+        Object key = "key";
+        assertNull( get( key ) );
+        assertFalse( set( key, "wrong", "value" ) );
+        assertNull( get( key ) );
+        assertTrue( set( key, null, "value" ) );
+        assertEquals( "value", get( key ) );
+        assertTrue( set( key, "value", "value" ) );
+        assertEquals( "value", get( key ) );
+        assertFalse( set( key, "wrong", "changed" ) );
+        assertEquals( "value", get( key ) );
+        assertTrue( set( key, "value", "changed" ) );
+        assertEquals( "changed", get( key ) );
+        assertFalse( set( key, "wrong", null ) );
+        assertEquals( "changed", get( key ) );
+        assertTrue( set( key, "changed", null ) );
+        assertNull( get( key ) );
+        assertTrue( set( key, null, null ) );
+        assertNull( get( key ) );
+    }
+
+    @Test( timeout = 10000L )
+    public void testConcurrency()
+        throws Exception
+    {
+        final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
+        Thread threads[] = new Thread[20];
+        for ( int i = 0; i < threads.length; i++ )
+        {
+            threads[i] = new Thread()
+            {
+                @Override
+                public void run()
+                {
+                    for ( int i = 0; i < 100; i++ )
+                    {
+                        String key = UUID.randomUUID().toString();
+                        try
+                        {
+                            set( key, Boolean.TRUE );
+                            assertEquals( Boolean.TRUE, get( key ) );
+                        }
+                        catch ( Throwable t )
+                        {
+                            error.compareAndSet( null, t );
+                            t.printStackTrace();
+                        }
+                    }
+                }
+            };
+        }
+        for ( Thread thread : threads )
+        {
+            thread.start();
+        }
+        for ( Thread thread : threads )
+        {
+            thread.join();
+        }
+        assertNull( String.valueOf( error.get() ), error.get() );
+    }
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/RepositoryExceptionTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/RepositoryExceptionTest.java
new file mode 100644
index 0000000..c3246be
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/RepositoryExceptionTest.java
@@ -0,0 +1,228 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.collection.UnsolvableVersionConflictException;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.junit.Test;
+
+public class RepositoryExceptionTest
+{
+
+    private void assertSerializable( RepositoryException e )
+    {
+        try
+        {
+            ObjectOutputStream oos = new ObjectOutputStream( new ByteArrayOutputStream() );
+            oos.writeObject( e );
+            oos.close();
+        }
+        catch ( IOException ioe )
+        {
+            throw new IllegalStateException( ioe );
+        }
+    }
+
+    private RequestTrace newTrace()
+    {
+        return new RequestTrace( "test" );
+    }
+
+    private Artifact newArtifact()
+    {
+        return new DefaultArtifact( "gid", "aid", "ext", "1" );
+    }
+
+    private Metadata newMetadata()
+    {
+        return new DefaultMetadata( "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+    }
+
+    private RemoteRepository newRepo()
+    {
+        Proxy proxy = new Proxy( Proxy.TYPE_HTTP, "localhost", 8080, null );
+        return new RemoteRepository.Builder( "id", "test", "http://localhost" ).setProxy( proxy ).build();
+    }
+
+    @Test
+    public void testArtifactDescriptorException_Serializable()
+    {
+        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest();
+        request.setArtifact( newArtifact() ).addRepository( newRepo() ).setTrace( newTrace() );
+        ArtifactDescriptorResult result = new ArtifactDescriptorResult( request );
+        assertSerializable( new ArtifactDescriptorException( result ) );
+    }
+
+    @Test
+    public void testArtifactResolutionException_Serializable()
+    {
+        ArtifactRequest request = new ArtifactRequest();
+        request.setArtifact( newArtifact() ).addRepository( newRepo() ).setTrace( newTrace() );
+        ArtifactResult result = new ArtifactResult( request );
+        assertSerializable( new ArtifactResolutionException( Arrays.asList( result ) ) );
+    }
+
+    @Test
+    public void testArtifactTransferException_Serializable()
+    {
+        assertSerializable( new ArtifactTransferException( newArtifact(), newRepo(), "error" ) );
+    }
+
+    @Test
+    public void testArtifactNotFoundException_Serializable()
+    {
+        assertSerializable( new ArtifactNotFoundException( newArtifact(), newRepo(), "error" ) );
+    }
+
+    @Test
+    public void testDependencyCollectionException_Serializable()
+    {
+        CollectRequest request = new CollectRequest();
+        request.addDependency( new Dependency( newArtifact(), "compile" ) );
+        request.addRepository( newRepo() );
+        request.setTrace( newTrace() );
+        CollectResult result = new CollectResult( request );
+        assertSerializable( new DependencyCollectionException( result ) );
+    }
+
+    @Test
+    public void testDependencyResolutionException_Serializable()
+    {
+        CollectRequest request = new CollectRequest();
+        request.addDependency( new Dependency( newArtifact(), "compile" ) );
+        request.addRepository( newRepo() );
+        request.setTrace( newTrace() );
+        DependencyRequest req = new DependencyRequest();
+        req.setTrace( newTrace() );
+        req.setCollectRequest( request );
+        DependencyResult result = new DependencyResult( req );
+        assertSerializable( new DependencyResolutionException( result, null ) );
+    }
+
+    @Test
+    public void testMetadataTransferException_Serializable()
+    {
+        assertSerializable( new MetadataTransferException( newMetadata(), newRepo(), "error" ) );
+    }
+
+    @Test
+    public void testMetadataNotFoundException_Serializable()
+    {
+        assertSerializable( new MetadataNotFoundException( newMetadata(), newRepo(), "error" ) );
+    }
+
+    @Test
+    public void testNoLocalRepositoryManagerException_Serializable()
+    {
+        assertSerializable( new NoLocalRepositoryManagerException( new LocalRepository( "/tmp" ) ) );
+    }
+
+    @Test
+    public void testNoRepositoryConnectorException_Serializable()
+    {
+        assertSerializable( new NoRepositoryConnectorException( newRepo() ) );
+    }
+
+    @Test
+    public void testNoRepositoryLayoutException_Serializable()
+    {
+        assertSerializable( new NoRepositoryLayoutException( newRepo() ) );
+    }
+
+    @Test
+    public void testNoTransporterException_Serializable()
+    {
+        assertSerializable( new NoTransporterException( newRepo() ) );
+    }
+
+    @Test
+    public void testRepositoryOfflineException_Serializable()
+    {
+        assertSerializable( new RepositoryOfflineException( newRepo() ) );
+    }
+
+    @Test
+    public void testUnsolvableVersionConflictException_Serializable()
+    {
+        DependencyNode node = new DefaultDependencyNode( new Dependency( newArtifact(), "test" ) );
+        assertSerializable( new UnsolvableVersionConflictException( Collections.singleton( Arrays.asList( node ) ) ) );
+    }
+
+    @Test
+    public void testVersionResolutionException_Serializable()
+    {
+        VersionRequest request = new VersionRequest();
+        request.setArtifact( newArtifact() ).addRepository( newRepo() ).setTrace( newTrace() );
+        VersionResult result = new VersionResult( request );
+        assertSerializable( new VersionResolutionException( result ) );
+    }
+
+    @Test
+    public void testVersionRangeResolutionException_Serializable()
+    {
+        VersionRangeRequest request = new VersionRangeRequest();
+        request.setArtifact( newArtifact() ).addRepository( newRepo() ).setTrace( newTrace() );
+        VersionRangeResult result = new VersionRangeResult( request );
+        assertSerializable( new VersionRangeResolutionException( result ) );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/RequestTraceTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/RequestTraceTest.java
new file mode 100644
index 0000000..63e5877
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/RequestTraceTest.java
@@ -0,0 +1,62 @@
+package org.eclipse.aether;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ */
+public class RequestTraceTest
+{
+
+    @Test
+    public void testConstructor()
+    {
+        RequestTrace trace = new RequestTrace( null );
+        assertSame( null, trace.getData() );
+
+        trace = new RequestTrace( this );
+        assertSame( this, trace.getData() );
+    }
+
+    @Test
+    public void testParentChaining()
+    {
+        RequestTrace trace1 = new RequestTrace( null );
+        RequestTrace trace2 = trace1.newChild( this );
+
+        assertSame( null, trace1.getParent() );
+        assertSame( null, trace1.getData() );
+        assertSame( trace1, trace2.getParent() );
+        assertSame( this, trace2.getData() );
+    }
+
+    @Test
+    public void testNewChildRequestTrace()
+    {
+        RequestTrace trace = RequestTrace.newChild( null, this );
+        assertNotNull( trace );
+        assertSame( null, trace.getParent() );
+        assertSame( this, trace.getData() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/artifact/DefaultArtifactTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/artifact/DefaultArtifactTest.java
new file mode 100644
index 0000000..d8ac40c
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/artifact/DefaultArtifactTest.java
@@ -0,0 +1,188 @@
+package org.eclipse.aether.artifact;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultArtifactTest
+{
+
+    @Test
+    public void testDefaultArtifactString()
+    {
+        Artifact a;
+
+        a = new DefaultArtifact( "gid:aid:ver" );
+        assertEquals( "gid", a.getGroupId() );
+        assertEquals( "aid", a.getArtifactId() );
+        assertEquals( "ver", a.getVersion() );
+        assertEquals( "ver", a.getBaseVersion() );
+        assertEquals( "jar", a.getExtension() );
+        assertEquals( "", a.getClassifier() );
+
+        a = new DefaultArtifact( "gid:aid:ext:ver" );
+        assertEquals( "gid", a.getGroupId() );
+        assertEquals( "aid", a.getArtifactId() );
+        assertEquals( "ver", a.getVersion() );
+        assertEquals( "ver", a.getBaseVersion() );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "", a.getClassifier() );
+
+        a = new DefaultArtifact( "org.gid:foo-bar:jar:1.1-20101116.150650-3" );
+        assertEquals( "org.gid", a.getGroupId() );
+        assertEquals( "foo-bar", a.getArtifactId() );
+        assertEquals( "1.1-20101116.150650-3", a.getVersion() );
+        assertEquals( "1.1-SNAPSHOT", a.getBaseVersion() );
+        assertEquals( "jar", a.getExtension() );
+        assertEquals( "", a.getClassifier() );
+
+        a = new DefaultArtifact( "gid:aid:ext:cls:ver" );
+        assertEquals( "gid", a.getGroupId() );
+        assertEquals( "aid", a.getArtifactId() );
+        assertEquals( "ver", a.getVersion() );
+        assertEquals( "ver", a.getBaseVersion() );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+
+        a = new DefaultArtifact( "gid:aid::cls:ver" );
+        assertEquals( "gid", a.getGroupId() );
+        assertEquals( "aid", a.getArtifactId() );
+        assertEquals( "ver", a.getVersion() );
+        assertEquals( "ver", a.getBaseVersion() );
+        assertEquals( "jar", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+
+        a = new DefaultArtifact( new DefaultArtifact( "gid:aid:ext:cls:ver" ).toString() );
+        assertEquals( "gid", a.getGroupId() );
+        assertEquals( "aid", a.getArtifactId() );
+        assertEquals( "ver", a.getVersion() );
+        assertEquals( "ver", a.getBaseVersion() );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void testDefaultArtifactBadString()
+    {
+        new DefaultArtifact( "gid:aid" );
+    }
+
+    @Test
+    public void testImmutability()
+    {
+        Artifact a = new DefaultArtifact( "gid:aid:ext:cls:ver" );
+        assertNotSame( a, a.setFile( new File( "file" ) ) );
+        assertNotSame( a, a.setVersion( "otherVersion" ) );
+        assertNotSame( a, a.setProperties( Collections.singletonMap( "key", "value" ) ) );
+    }
+
+    @Test
+    public void testArtifactType()
+    {
+        DefaultArtifactType type = new DefaultArtifactType( "typeId", "typeExt", "typeCls", "typeLang", true, true );
+
+        Artifact a = new DefaultArtifact( "gid", "aid", null, null, null, null, type );
+        assertEquals( "typeExt", a.getExtension() );
+        assertEquals( "typeCls", a.getClassifier() );
+        assertEquals( "typeLang", a.getProperties().get( ArtifactProperties.LANGUAGE ) );
+        assertEquals( "typeId", a.getProperties().get( ArtifactProperties.TYPE ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.INCLUDES_DEPENDENCIES ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.CONSTITUTES_BUILD_PATH ) );
+
+        a = new DefaultArtifact( "gid", "aid", "cls", "ext", "ver", null, type );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+        assertEquals( "typeLang", a.getProperties().get( ArtifactProperties.LANGUAGE ) );
+        assertEquals( "typeId", a.getProperties().get( ArtifactProperties.TYPE ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.INCLUDES_DEPENDENCIES ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.CONSTITUTES_BUILD_PATH ) );
+
+        Map<String, String> props = new HashMap<String, String>();
+        props.put( "someNonStandardProperty", "someNonStandardProperty" );
+        a = new DefaultArtifact( "gid", "aid", "cls", "ext", "ver", props, type );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+        assertEquals( "typeLang", a.getProperties().get( ArtifactProperties.LANGUAGE ) );
+        assertEquals( "typeId", a.getProperties().get( ArtifactProperties.TYPE ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.INCLUDES_DEPENDENCIES ) );
+        assertEquals( "true", a.getProperties().get( ArtifactProperties.CONSTITUTES_BUILD_PATH ) );
+        assertEquals( "someNonStandardProperty", a.getProperties().get( "someNonStandardProperty" ) );
+
+        props = new HashMap<String, String>();
+        props.put( "someNonStandardProperty", "someNonStandardProperty" );
+        props.put( ArtifactProperties.CONSTITUTES_BUILD_PATH, "rubbish" );
+        props.put( ArtifactProperties.INCLUDES_DEPENDENCIES, "rubbish" );
+        a = new DefaultArtifact( "gid", "aid", "cls", "ext", "ver", props, type );
+        assertEquals( "ext", a.getExtension() );
+        assertEquals( "cls", a.getClassifier() );
+        assertEquals( "typeLang", a.getProperties().get( ArtifactProperties.LANGUAGE ) );
+        assertEquals( "typeId", a.getProperties().get( ArtifactProperties.TYPE ) );
+        assertEquals( "rubbish", a.getProperties().get( ArtifactProperties.INCLUDES_DEPENDENCIES ) );
+        assertEquals( "rubbish", a.getProperties().get( ArtifactProperties.CONSTITUTES_BUILD_PATH ) );
+        assertEquals( "someNonStandardProperty", a.getProperties().get( "someNonStandardProperty" ) );
+    }
+
+    @Test
+    public void testPropertiesCopied()
+    {
+        Map<String, String> props = new HashMap<String, String>();
+        props.put( "key", "value1" );
+
+        Artifact a = new DefaultArtifact( "gid:aid:1", props );
+        assertEquals( "value1", a.getProperty( "key", null ) );
+        props.clear();
+        assertEquals( "value1", a.getProperty( "key", null ) );
+
+        props.put( "key", "value2" );
+        a = a.setProperties( props );
+        assertEquals( "value2", a.getProperty( "key", null ) );
+        props.clear();
+        assertEquals( "value2", a.getProperty( "key", null ) );
+    }
+
+    @Test
+    public void testIsSnapshot()
+    {
+        Artifact a = new DefaultArtifact( "gid:aid:ext:cls:1.0" );
+        assertFalse( a.getVersion(), a.isSnapshot() );
+
+        a = new DefaultArtifact( "gid:aid:ext:cls:1.0-SNAPSHOT" );
+        assertTrue( a.getVersion(), a.isSnapshot() );
+
+        a = new DefaultArtifact( "gid:aid:ext:cls:1.0-20101116.150650-3" );
+        assertTrue( a.getVersion(), a.isSnapshot() );
+
+        a = new DefaultArtifact( "gid:aid:ext:cls:1.0-20101116x150650-3" );
+        assertFalse( a.getVersion(), a.isSnapshot() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/graph/DependencyTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/graph/DependencyTest.java
new file mode 100644
index 0000000..c96746d
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/graph/DependencyTest.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.graph;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.junit.Test;
+
+/**
+ */
+public class DependencyTest
+{
+
+    @Test
+    public void testSetScope()
+    {
+        Dependency d1 = new Dependency( new DefaultArtifact( "gid:aid:ver" ), "compile" );
+
+        Dependency d2 = d1.setScope( null );
+        assertNotSame( d2, d1 );
+        assertEquals( "", d2.getScope() );
+
+        Dependency d3 = d1.setScope( "test" );
+        assertNotSame( d3, d1 );
+        assertEquals( "test", d3.getScope() );
+    }
+
+    @Test
+    public void testSetExclusions()
+    {
+        Dependency d1 =
+            new Dependency( new DefaultArtifact( "gid:aid:ver" ), "compile", false,
+                            Collections.singleton( new Exclusion( "g", "a", "c", "e" ) ) );
+
+        Dependency d2 = d1.setExclusions( null );
+        assertNotSame( d2, d1 );
+        assertEquals( 0, d2.getExclusions().size() );
+
+        assertSame( d2, d2.setExclusions( null ) );
+        assertSame( d2, d2.setExclusions( Collections.<Exclusion> emptyList() ) );
+        assertSame( d2, d2.setExclusions( Collections.<Exclusion> emptySet() ) );
+        assertSame( d1, d1.setExclusions( Arrays.asList( new Exclusion( "g", "a", "c", "e" ) ) ) );
+
+        Dependency d3 =
+            d1.setExclusions( Arrays.asList( new Exclusion( "g", "a", "c", "e" ), new Exclusion( "g", "a", "c", "f" ) ) );
+        assertNotSame( d3, d1 );
+        assertEquals( 2, d3.getExclusions().size() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationContextTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationContextTest.java
new file mode 100644
index 0000000..6d579a1
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationContextTest.java
@@ -0,0 +1,170 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.util.Map;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.junit.Test;
+
+public class AuthenticationContextTest
+{
+
+    private RepositorySystemSession newSession()
+    {
+        return new DefaultRepositorySystemSession();
+    }
+
+    private RemoteRepository newRepo( Authentication auth, Proxy proxy )
+    {
+        return new RemoteRepository.Builder( "test", "default", "http://localhost" ) //
+        .setAuthentication( auth ).setProxy( proxy ).build();
+    }
+
+    private Proxy newProxy( Authentication auth )
+    {
+        return new Proxy( Proxy.TYPE_HTTP, "localhost", 8080, auth );
+    }
+
+    private Authentication newAuth()
+    {
+        return new Authentication()
+        {
+            public void fill( AuthenticationContext context, String key, Map<String, String> data )
+            {
+                assertNotNull( context );
+                assertNotNull( context.getSession() );
+                assertNotNull( context.getRepository() );
+                assertNull( "fill() should only be called once", context.get( "key" ) );
+                context.put( "key", "value" );
+            }
+
+            public void digest( AuthenticationDigest digest )
+            {
+                fail( "AuthenticationContext should not call digest()" );
+            }
+        };
+    }
+
+    @Test
+    public void testForRepository()
+    {
+        RepositorySystemSession session = newSession();
+        RemoteRepository repo = newRepo( newAuth(), newProxy( newAuth() ) );
+        AuthenticationContext context = AuthenticationContext.forRepository( session, repo );
+        assertNotNull( context );
+        assertSame( session, context.getSession() );
+        assertSame( repo, context.getRepository() );
+        assertNull( context.getProxy() );
+        assertEquals( "value", context.get( "key" ) );
+        assertEquals( "value", context.get( "key" ) );
+    }
+
+    @Test
+    public void testForRepository_NoAuth()
+    {
+        RepositorySystemSession session = newSession();
+        RemoteRepository repo = newRepo( null, newProxy( newAuth() ) );
+        AuthenticationContext context = AuthenticationContext.forRepository( session, repo );
+        assertNull( context );
+    }
+
+    @Test
+    public void testForProxy()
+    {
+        RepositorySystemSession session = newSession();
+        Proxy proxy = newProxy( newAuth() );
+        RemoteRepository repo = newRepo( newAuth(), proxy );
+        AuthenticationContext context = AuthenticationContext.forProxy( session, repo );
+        assertNotNull( context );
+        assertSame( session, context.getSession() );
+        assertSame( repo, context.getRepository() );
+        assertSame( proxy, context.getProxy() );
+        assertEquals( "value", context.get( "key" ) );
+        assertEquals( "value", context.get( "key" ) );
+    }
+
+    @Test
+    public void testForProxy_NoProxy()
+    {
+        RepositorySystemSession session = newSession();
+        Proxy proxy = null;
+        RemoteRepository repo = newRepo( newAuth(), proxy );
+        AuthenticationContext context = AuthenticationContext.forProxy( session, repo );
+        assertNull( context );
+    }
+
+    @Test
+    public void testForProxy_NoProxyAuth()
+    {
+        RepositorySystemSession session = newSession();
+        Proxy proxy = newProxy( null );
+        RemoteRepository repo = newRepo( newAuth(), proxy );
+        AuthenticationContext context = AuthenticationContext.forProxy( session, repo );
+        assertNull( context );
+    }
+
+    @Test
+    public void testGet_StringVsChars()
+    {
+        AuthenticationContext context = AuthenticationContext.forRepository( newSession(), newRepo( newAuth(), null ) );
+        context.put( "key", new char[] { 'v', 'a', 'l', '1' } );
+        assertEquals( "val1", context.get( "key" ) );
+        context.put( "key", "val2" );
+        assertArrayEquals( new char[] { 'v', 'a', 'l', '2' }, context.get( "key", char[].class ) );
+    }
+
+    @Test
+    public void testGet_StringVsFile()
+    {
+        AuthenticationContext context = AuthenticationContext.forRepository( newSession(), newRepo( newAuth(), null ) );
+        context.put( "key", "val1" );
+        assertEquals( new File( "val1" ), context.get( "key", File.class ) );
+        context.put( "key", new File( "val2" ) );
+        assertEquals( "val2", context.get( "key" ) );
+    }
+
+    @Test
+    public void testPut_EraseCharArrays()
+    {
+        AuthenticationContext context = AuthenticationContext.forRepository( newSession(), newRepo( newAuth(), null ) );
+        char[] secret = { 'v', 'a', 'l', 'u', 'e' };
+        context.put( "key", secret );
+        context.put( "key", secret.clone() );
+        assertArrayEquals( new char[] { 0, 0, 0, 0, 0 }, secret );
+    }
+
+    @Test
+    public void testClose_EraseCharArrays()
+    {
+        AuthenticationContext.close( null );
+
+        AuthenticationContext context = AuthenticationContext.forRepository( newSession(), newRepo( newAuth(), null ) );
+        char[] secret = { 'v', 'a', 'l', 'u', 'e' };
+        context.put( "key", secret );
+        AuthenticationContext.close( context );
+        assertArrayEquals( new char[] { 0, 0, 0, 0, 0 }, secret );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationDigestTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationDigestTest.java
new file mode 100644
index 0000000..387a3da
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/AuthenticationDigestTest.java
@@ -0,0 +1,150 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Map;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.junit.Test;
+
+public class AuthenticationDigestTest
+{
+
+    private RepositorySystemSession newSession()
+    {
+        return new DefaultRepositorySystemSession();
+    }
+
+    private RemoteRepository newRepo( Authentication auth, Proxy proxy )
+    {
+        return new RemoteRepository.Builder( "test", "default", "http://localhost" ) //
+        .setAuthentication( auth ).setProxy( proxy ).build();
+    }
+
+    private Proxy newProxy( Authentication auth )
+    {
+        return new Proxy( Proxy.TYPE_HTTP, "localhost", 8080, auth );
+    }
+
+    @Test
+    public void testForRepository()
+    {
+        final RepositorySystemSession session = newSession();
+        final RemoteRepository[] repos = { null };
+
+        Authentication auth = new Authentication()
+        {
+            public void fill( AuthenticationContext context, String key, Map<String, String> data )
+            {
+                fail( "AuthenticationDigest should not call fill()" );
+            }
+
+            public void digest( AuthenticationDigest digest )
+            {
+                assertNotNull( digest );
+                assertSame( session, digest.getSession() );
+                assertNotNull( digest.getRepository() );
+                assertNull( digest.getProxy() );
+                assertNull( "digest() should only be called once", repos[0] );
+                repos[0] = digest.getRepository();
+
+                digest.update( (byte[]) null );
+                digest.update( (char[]) null );
+                digest.update( (String[]) null );
+                digest.update( null, null );
+            }
+        };
+
+        RemoteRepository repo = newRepo( auth, newProxy( null ) );
+
+        String digest = AuthenticationDigest.forRepository( session, repo );
+        assertSame( repo, repos[0] );
+        assertNotNull( digest );
+        assertTrue( digest.length() > 0 );
+    }
+
+    @Test
+    public void testForRepository_NoAuth()
+    {
+        RemoteRepository repo = newRepo( null, null );
+
+        String digest = AuthenticationDigest.forRepository( newSession(), repo );
+        assertEquals( "", digest );
+    }
+
+    @Test
+    public void testForProxy()
+    {
+        final RepositorySystemSession session = newSession();
+        final Proxy[] proxies = { null };
+
+        Authentication auth = new Authentication()
+        {
+            public void fill( AuthenticationContext context, String key, Map<String, String> data )
+            {
+                fail( "AuthenticationDigest should not call fill()" );
+            }
+
+            public void digest( AuthenticationDigest digest )
+            {
+                assertNotNull( digest );
+                assertSame( session, digest.getSession() );
+                assertNotNull( digest.getRepository() );
+                assertNotNull( digest.getProxy() );
+                assertNull( "digest() should only be called once", proxies[0] );
+                proxies[0] = digest.getProxy();
+
+                digest.update( (byte[]) null );
+                digest.update( (char[]) null );
+                digest.update( (String[]) null );
+                digest.update( null, null );
+            }
+        };
+
+        Proxy proxy = newProxy( auth );
+
+        String digest = AuthenticationDigest.forProxy( session, newRepo( null, proxy ) );
+        assertSame( proxy, proxies[0] );
+        assertNotNull( digest );
+        assertTrue( digest.length() > 0 );
+    }
+
+    @Test
+    public void testForProxy_NoProxy()
+    {
+        RemoteRepository repo = newRepo( null, null );
+
+        String digest = AuthenticationDigest.forProxy( newSession(), repo );
+        assertEquals( "", digest );
+    }
+
+    @Test
+    public void testForProxy_NoProxyAuth()
+    {
+        RemoteRepository repo = newRepo( null, newProxy( null ) );
+
+        String digest = AuthenticationDigest.forProxy( newSession(), repo );
+        assertEquals( "", digest );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryBuilderTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryBuilderTest.java
new file mode 100644
index 0000000..e2c15e3
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryBuilderTest.java
@@ -0,0 +1,185 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.repository.RemoteRepository.Builder;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RemoteRepositoryBuilderTest
+{
+
+    private RemoteRepository prototype;
+
+    @Before
+    public void init()
+    {
+        prototype = new Builder( "id", "type", "file:void" ).build();
+    }
+
+    @Test
+    public void testReusePrototype()
+    {
+        Builder builder = new Builder( prototype );
+        assertSame( prototype, builder.build() );
+    }
+
+    @Test( expected = NullPointerException.class )
+    public void testPrototypeMandatory()
+    {
+        new Builder( null );
+    }
+
+    @Test
+    public void testSetId()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setId( prototype.getId() ).build();
+        assertSame( prototype, repo );
+        repo = builder.setId( "new-id" ).build();
+        assertEquals( "new-id", repo.getId() );
+    }
+
+    @Test
+    public void testSetContentType()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setContentType( prototype.getContentType() ).build();
+        assertSame( prototype, repo );
+        repo = builder.setContentType( "new-type" ).build();
+        assertEquals( "new-type", repo.getContentType() );
+    }
+
+    @Test
+    public void testSetUrl()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setUrl( prototype.getUrl() ).build();
+        assertSame( prototype, repo );
+        repo = builder.setUrl( "file:new" ).build();
+        assertEquals( "file:new", repo.getUrl() );
+    }
+
+    @Test
+    public void testSetPolicy()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setPolicy( prototype.getPolicy( false ) ).build();
+        assertSame( prototype, repo );
+        RepositoryPolicy policy = new RepositoryPolicy( true, "never", "fail" );
+        repo = builder.setPolicy( policy ).build();
+        assertEquals( policy, repo.getPolicy( true ) );
+        assertEquals( policy, repo.getPolicy( false ) );
+    }
+
+    @Test
+    public void testSetReleasePolicy()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setReleasePolicy( prototype.getPolicy( false ) ).build();
+        assertSame( prototype, repo );
+        RepositoryPolicy policy = new RepositoryPolicy( true, "never", "fail" );
+        repo = builder.setReleasePolicy( policy ).build();
+        assertEquals( policy, repo.getPolicy( false ) );
+        assertEquals( prototype.getPolicy( true ), repo.getPolicy( true ) );
+    }
+
+    @Test
+    public void testSetSnapshotPolicy()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setSnapshotPolicy( prototype.getPolicy( true ) ).build();
+        assertSame( prototype, repo );
+        RepositoryPolicy policy = new RepositoryPolicy( true, "never", "fail" );
+        repo = builder.setSnapshotPolicy( policy ).build();
+        assertEquals( policy, repo.getPolicy( true ) );
+        assertEquals( prototype.getPolicy( false ), repo.getPolicy( false ) );
+    }
+
+    @Test
+    public void testSetProxy()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setProxy( prototype.getProxy() ).build();
+        assertSame( prototype, repo );
+        Proxy proxy = new Proxy( "http", "localhost", 8080 );
+        repo = builder.setProxy( proxy ).build();
+        assertEquals( proxy, repo.getProxy() );
+    }
+
+    @Test
+    public void testSetAuthentication()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setAuthentication( prototype.getAuthentication() ).build();
+        assertSame( prototype, repo );
+        Authentication auth = new Authentication()
+        {
+            public void fill( AuthenticationContext context, String key, Map<String, String> data )
+            {
+            }
+
+            public void digest( AuthenticationDigest digest )
+            {
+            }
+        };
+        repo = builder.setAuthentication( auth ).build();
+        assertEquals( auth, repo.getAuthentication() );
+    }
+
+    @Test
+    public void testSetMirroredRepositories()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setMirroredRepositories( prototype.getMirroredRepositories() ).build();
+        assertSame( prototype, repo );
+        List<RemoteRepository> mirrored = new ArrayList<RemoteRepository>( Arrays.asList( repo ) );
+        repo = builder.setMirroredRepositories( mirrored ).build();
+        assertEquals( mirrored, repo.getMirroredRepositories() );
+    }
+
+    @Test
+    public void testAddMirroredRepository()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.addMirroredRepository( null ).build();
+        assertSame( prototype, repo );
+        repo = builder.addMirroredRepository( prototype ).build();
+        assertEquals( Arrays.asList( prototype ), repo.getMirroredRepositories() );
+    }
+
+    @Test
+    public void testSetRepositoryManager()
+    {
+        Builder builder = new Builder( prototype );
+        RemoteRepository repo = builder.setRepositoryManager( prototype.isRepositoryManager() ).build();
+        assertSame( prototype, repo );
+        repo = builder.setRepositoryManager( !prototype.isRepositoryManager() ).build();
+        assertEquals( !prototype.isRepositoryManager(), repo.isRepositoryManager() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryTest.java
new file mode 100644
index 0000000..97f0b3e
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/RemoteRepositoryTest.java
@@ -0,0 +1,96 @@
+package org.eclipse.aether.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+/**
+ */
+public class RemoteRepositoryTest
+{
+
+    @Test
+    public void testGetProtocol()
+    {
+        RemoteRepository.Builder builder = new RemoteRepository.Builder( "id", "type", "" );
+        RemoteRepository repo = builder.build();
+        assertEquals( "", repo.getProtocol() );
+
+        repo = builder.setUrl( "http://localhost" ).build();
+        assertEquals( "http", repo.getProtocol() );
+
+        repo = builder.setUrl( "HTTP://localhost" ).build();
+        assertEquals( "HTTP", repo.getProtocol() );
+
+        repo = builder.setUrl( "dav+http://www.sonatype.org/" ).build();
+        assertEquals( "dav+http", repo.getProtocol() );
+
+        repo = builder.setUrl( "dav:http://www.sonatype.org/" ).build();
+        assertEquals( "dav:http", repo.getProtocol() );
+
+        repo = builder.setUrl( "file:/path" ).build();
+        assertEquals( "file", repo.getProtocol() );
+
+        repo = builder.setUrl( "file:path" ).build();
+        assertEquals( "file", repo.getProtocol() );
+
+        repo = builder.setUrl( "file:C:\\dir" ).build();
+        assertEquals( "file", repo.getProtocol() );
+
+        repo = builder.setUrl( "file:C:/dir" ).build();
+        assertEquals( "file", repo.getProtocol() );
+    }
+
+    @Test
+    public void testGetHost()
+    {
+        RemoteRepository.Builder builder = new RemoteRepository.Builder( "id", "type", "" );
+        RemoteRepository repo = builder.build();
+        assertEquals( "", repo.getHost() );
+
+        repo = builder.setUrl( "http://localhost" ).build();
+        assertEquals( "localhost", repo.getHost() );
+
+        repo = builder.setUrl( "http://localhost/" ).build();
+        assertEquals( "localhost", repo.getHost() );
+
+        repo = builder.setUrl( "http://localhost:1234/" ).build();
+        assertEquals( "localhost", repo.getHost() );
+
+        repo = builder.setUrl( "http://127.0.0.1" ).build();
+        assertEquals( "127.0.0.1", repo.getHost() );
+
+        repo = builder.setUrl( "http://127.0.0.1/" ).build();
+        assertEquals( "127.0.0.1", repo.getHost() );
+
+        repo = builder.setUrl( "http://user@localhost/path" ).build();
+        assertEquals( "localhost", repo.getHost() );
+
+        repo = builder.setUrl( "http://user:pass@localhost/path" ).build();
+        assertEquals( "localhost", repo.getHost() );
+
+        repo = builder.setUrl( "http://user:pass@localhost:1234/path" ).build();
+        assertEquals( "localhost", repo.getHost() );
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/AbstractTransferListenerTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/AbstractTransferListenerTest.java
new file mode 100644
index 0000000..87c1472
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/AbstractTransferListenerTest.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferListener;
+import org.junit.Test;
+
+/**
+ */
+public class AbstractTransferListenerTest
+{
+
+    @Test
+    public void testAllEventTypesHandled()
+        throws Exception
+    {
+        for ( Method method : TransferListener.class.getMethods() )
+        {
+            assertNotNull( AbstractTransferListener.class.getDeclaredMethod( method.getName(),
+                                                                             method.getParameterTypes() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java
new file mode 100644
index 0000000..7d4c070
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java
@@ -0,0 +1,85 @@
+package org.eclipse.aether.transfer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferResource;
+import org.junit.Test;
+
+/**
+ */
+public class TransferEventTest
+{
+
+    private static TransferResource res = new TransferResource( "none", "file://nil", "void", null, null );
+
+    private static RepositorySystemSession session = new DefaultRepositorySystemSession();
+
+    @Test
+    public void testByteArrayConversion()
+    {
+        byte[] buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        int length = buffer.length - 2;
+        int offset = 1;
+
+        TransferEvent event = new TransferEvent.Builder( session, res ).setDataBuffer( buffer, offset, length ).build();
+
+        ByteBuffer bb = event.getDataBuffer();
+        byte[] dst = new byte[bb.remaining()];
+        bb.get( dst );
+
+        byte[] expected = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
+        assertArrayEquals( expected, dst );
+    }
+
+    @Test
+    public void testRepeatableReadingOfDataBuffer()
+    {
+        byte[] data = { 0, 1, 2, 3, 4, 5, 6, 7 };
+        ByteBuffer buffer = ByteBuffer.wrap( data );
+
+        TransferEvent event = new TransferEvent.Builder( session, res ).setDataBuffer( buffer ).build();
+
+        assertEquals( 8, event.getDataLength() );
+
+        ByteBuffer eventBuffer = event.getDataBuffer();
+        assertNotNull( eventBuffer );
+        assertEquals( 8, eventBuffer.remaining() );
+
+        byte[] eventData = new byte[8];
+        eventBuffer.get( eventData );
+        assertArrayEquals( data, eventData );
+        assertEquals( 0, eventBuffer.remaining() );
+        assertEquals( 8, event.getDataLength() );
+
+        eventBuffer = event.getDataBuffer();
+        assertNotNull( eventBuffer );
+        assertEquals( 8, eventBuffer.remaining() );
+        eventBuffer.get( eventData );
+        assertArrayEquals( data, eventData );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/pom.xml b/maven-resolver-connector-basic/pom.xml
new file mode 100644
index 0000000..b2a936a
--- /dev/null
+++ b/maven-resolver-connector-basic/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-connector-basic</artifactId>
+
+  <name>Maven Artifact Resolver Connector Basic</name>
+  <description>
+      A repository connector implementation for repositories using URI-based layouts.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.connector.basic</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ArtifactTransportListener.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ArtifactTransportListener.java
new file mode 100644
index 0000000..f8a9b1c
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ArtifactTransportListener.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.ArtifactTransfer;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.TransferEvent;
+
+final class ArtifactTransportListener
+    extends TransferTransportListener<ArtifactTransfer>
+{
+
+    private final RemoteRepository repository;
+
+    public ArtifactTransportListener( ArtifactTransfer transfer, RemoteRepository repository,
+                                      TransferEvent.Builder eventBuilder )
+    {
+        super( transfer, eventBuilder );
+        this.repository = repository;
+    }
+
+    @Override
+    public void transferFailed( Exception exception, int classification )
+    {
+        ArtifactTransferException e;
+        if ( classification == Transporter.ERROR_NOT_FOUND )
+        {
+            e = new ArtifactNotFoundException( getTransfer().getArtifact(), repository );
+        }
+        else
+        {
+            e = new ArtifactTransferException( getTransfer().getArtifact(), repository, exception );
+        }
+        getTransfer().setException( e );
+        super.transferFailed( e, classification );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
new file mode 100644
index 0000000..a3cce25
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
@@ -0,0 +1,588 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.ArtifactUpload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.MetadataUpload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterProvider;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferResource;
+import org.eclipse.aether.util.ChecksumUtils;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.concurrency.RunnableErrorForwarder;
+import org.eclipse.aether.util.concurrency.WorkerThreadFactory;
+
+/**
+ */
+final class BasicRepositoryConnector
+    implements RepositoryConnector
+{
+
+    private static final String CONFIG_PROP_THREADS = "aether.connector.basic.threads";
+
+    private static final String CONFIG_PROP_RESUME = "aether.connector.resumeDownloads";
+
+    private static final String CONFIG_PROP_RESUME_THRESHOLD = "aether.connector.resumeThreshold";
+
+    private static final String CONFIG_PROP_SMART_CHECKSUMS = "aether.connector.smartChecksums";
+
+    private final Logger logger;
+
+    private final FileProcessor fileProcessor;
+
+    private final RemoteRepository repository;
+
+    private final RepositorySystemSession session;
+
+    private final Transporter transporter;
+
+    private final RepositoryLayout layout;
+
+    private final ChecksumPolicyProvider checksumPolicyProvider;
+
+    private final PartialFile.Factory partialFileFactory;
+
+    private final int maxThreads;
+
+    private final boolean smartChecksums;
+
+    private final boolean persistedChecksums;
+
+    private Executor executor;
+
+    private boolean closed;
+
+    public BasicRepositoryConnector( RepositorySystemSession session, RemoteRepository repository,
+                                     TransporterProvider transporterProvider, RepositoryLayoutProvider layoutProvider,
+                                     ChecksumPolicyProvider checksumPolicyProvider, FileProcessor fileProcessor,
+                                     Logger logger )
+        throws NoRepositoryConnectorException
+    {
+        try
+        {
+            layout = layoutProvider.newRepositoryLayout( session, repository );
+        }
+        catch ( NoRepositoryLayoutException e )
+        {
+            throw new NoRepositoryConnectorException( repository, e.getMessage(), e );
+        }
+        try
+        {
+            transporter = transporterProvider.newTransporter( session, repository );
+        }
+        catch ( NoTransporterException e )
+        {
+            throw new NoRepositoryConnectorException( repository, e.getMessage(), e );
+        }
+        this.checksumPolicyProvider = checksumPolicyProvider;
+
+        this.session = session;
+        this.repository = repository;
+        this.fileProcessor = fileProcessor;
+        this.logger = logger;
+
+        maxThreads = ConfigUtils.getInteger( session, 5, CONFIG_PROP_THREADS, "maven.artifact.threads" );
+        smartChecksums = ConfigUtils.getBoolean( session, true, CONFIG_PROP_SMART_CHECKSUMS );
+        persistedChecksums =
+            ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_PERSISTED_CHECKSUMS,
+                                    ConfigurationProperties.PERSISTED_CHECKSUMS );
+
+        boolean resumeDownloads =
+            ConfigUtils.getBoolean( session, true, CONFIG_PROP_RESUME + '.' + repository.getId(), CONFIG_PROP_RESUME );
+        long resumeThreshold =
+            ConfigUtils.getLong( session, 64 * 1024, CONFIG_PROP_RESUME_THRESHOLD + '.' + repository.getId(),
+                                 CONFIG_PROP_RESUME_THRESHOLD );
+        int requestTimeout =
+            ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
+                                    ConfigurationProperties.REQUEST_TIMEOUT + '.' + repository.getId(),
+                                    ConfigurationProperties.REQUEST_TIMEOUT );
+        partialFileFactory = new PartialFile.Factory( resumeDownloads, resumeThreshold, requestTimeout, logger );
+    }
+
+    private Executor getExecutor( Collection<?> artifacts, Collection<?> metadatas )
+    {
+        if ( maxThreads <= 1 )
+        {
+            return DirectExecutor.INSTANCE;
+        }
+        int tasks = safe( artifacts ).size() + safe( metadatas ).size();
+        if ( tasks <= 1 )
+        {
+            return DirectExecutor.INSTANCE;
+        }
+        if ( executor == null )
+        {
+            executor =
+                new ThreadPoolExecutor( maxThreads, maxThreads, 3L, TimeUnit.SECONDS,
+                                        new LinkedBlockingQueue<Runnable>(),
+                                        new WorkerThreadFactory( getClass().getSimpleName() + '-'
+                                            + repository.getHost() + '-' ) );
+        }
+        return executor;
+    }
+
+    @Override
+    protected void finalize()
+        throws Throwable
+    {
+        try
+        {
+            close();
+        }
+        finally
+        {
+            super.finalize();
+        }
+    }
+
+    public void close()
+    {
+        if ( !closed )
+        {
+            closed = true;
+            if ( executor instanceof ExecutorService )
+            {
+                ( (ExecutorService) executor ).shutdown();
+            }
+            transporter.close();
+        }
+    }
+
+    public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                     Collection<? extends MetadataDownload> metadataDownloads )
+    {
+        if ( closed )
+        {
+            throw new IllegalStateException( "connector closed" );
+        }
+
+        Executor executor = getExecutor( artifactDownloads, metadataDownloads );
+        RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
+
+        for ( MetadataDownload transfer : safe( metadataDownloads ) )
+        {
+            URI location = layout.getLocation( transfer.getMetadata(), false );
+
+            TransferResource resource = newTransferResource( location, transfer.getFile(), transfer.getTrace() );
+            TransferEvent.Builder builder = newEventBuilder( resource, false, false );
+            MetadataTransportListener listener = new MetadataTransportListener( transfer, repository, builder );
+
+            ChecksumPolicy checksumPolicy = newChecksumPolicy( transfer.getChecksumPolicy(), resource );
+            List<RepositoryLayout.Checksum> checksums = null;
+            if ( checksumPolicy != null )
+            {
+                checksums = layout.getChecksums( transfer.getMetadata(), false, location );
+            }
+
+            Runnable task = new GetTaskRunner( location, transfer.getFile(), checksumPolicy, checksums, listener );
+            executor.execute( errorForwarder.wrap( task ) );
+        }
+
+        for ( ArtifactDownload transfer : safe( artifactDownloads ) )
+        {
+            URI location = layout.getLocation( transfer.getArtifact(), false );
+
+            TransferResource resource = newTransferResource( location, transfer.getFile(), transfer.getTrace() );
+            TransferEvent.Builder builder = newEventBuilder( resource, false, transfer.isExistenceCheck() );
+            ArtifactTransportListener listener = new ArtifactTransportListener( transfer, repository, builder );
+
+            Runnable task;
+            if ( transfer.isExistenceCheck() )
+            {
+                task = new PeekTaskRunner( location, listener );
+            }
+            else
+            {
+                ChecksumPolicy checksumPolicy = newChecksumPolicy( transfer.getChecksumPolicy(), resource );
+                List<RepositoryLayout.Checksum> checksums = null;
+                if ( checksumPolicy != null )
+                {
+                    checksums = layout.getChecksums( transfer.getArtifact(), false, location );
+                }
+
+                task = new GetTaskRunner( location, transfer.getFile(), checksumPolicy, checksums, listener );
+            }
+            executor.execute( errorForwarder.wrap( task ) );
+        }
+
+        errorForwarder.await();
+    }
+
+    public void put( Collection<? extends ArtifactUpload> artifactUploads,
+                     Collection<? extends MetadataUpload> metadataUploads )
+    {
+        if ( closed )
+        {
+            throw new IllegalStateException( "connector closed" );
+        }
+
+        for ( ArtifactUpload transfer : safe( artifactUploads ) )
+        {
+            URI location = layout.getLocation( transfer.getArtifact(), true );
+
+            TransferResource resource = newTransferResource( location, transfer.getFile(), transfer.getTrace() );
+            TransferEvent.Builder builder = newEventBuilder( resource, true, false );
+            ArtifactTransportListener listener = new ArtifactTransportListener( transfer, repository, builder );
+
+            List<RepositoryLayout.Checksum> checksums = layout.getChecksums( transfer.getArtifact(), true, location );
+
+            Runnable task = new PutTaskRunner( location, transfer.getFile(), checksums, listener );
+            task.run();
+        }
+
+        for ( MetadataUpload transfer : safe( metadataUploads ) )
+        {
+            URI location = layout.getLocation( transfer.getMetadata(), true );
+
+            TransferResource resource = newTransferResource( location, transfer.getFile(), transfer.getTrace() );
+            TransferEvent.Builder builder = newEventBuilder( resource, true, false );
+            MetadataTransportListener listener = new MetadataTransportListener( transfer, repository, builder );
+
+            List<RepositoryLayout.Checksum> checksums = layout.getChecksums( transfer.getMetadata(), true, location );
+
+            Runnable task = new PutTaskRunner( location, transfer.getFile(), checksums, listener );
+            task.run();
+        }
+    }
+
+    private static <T> Collection<T> safe( Collection<T> items )
+    {
+        return ( items != null ) ? items : Collections.<T>emptyList();
+    }
+
+    private TransferResource newTransferResource( URI path, File file, RequestTrace trace )
+    {
+        return new TransferResource( repository.getId(), repository.getUrl(), path.toString(), file, trace );
+    }
+
+    private TransferEvent.Builder newEventBuilder( TransferResource resource, boolean upload, boolean peek )
+    {
+        TransferEvent.Builder builder = new TransferEvent.Builder( session, resource );
+        if ( upload )
+        {
+            builder.setRequestType( TransferEvent.RequestType.PUT );
+        }
+        else if ( !peek )
+        {
+            builder.setRequestType( TransferEvent.RequestType.GET );
+        }
+        else
+        {
+            builder.setRequestType( TransferEvent.RequestType.GET_EXISTENCE );
+        }
+        return builder;
+    }
+
+    private ChecksumPolicy newChecksumPolicy( String policy, TransferResource resource )
+    {
+        return checksumPolicyProvider.newChecksumPolicy( session, repository, resource, policy );
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( repository );
+    }
+
+    abstract class TaskRunner
+        implements Runnable
+    {
+
+        protected final URI path;
+
+        protected final TransferTransportListener<?> listener;
+
+        public TaskRunner( URI path, TransferTransportListener<?> listener )
+        {
+            this.path = path;
+            this.listener = listener;
+        }
+
+        public void run()
+        {
+            try
+            {
+                listener.transferInitiated();
+                runTask();
+                listener.transferSucceeded();
+            }
+            catch ( Exception e )
+            {
+                listener.transferFailed( e, transporter.classify( e ) );
+            }
+        }
+
+        protected abstract void runTask()
+            throws Exception;
+
+    }
+
+    class PeekTaskRunner
+        extends TaskRunner
+    {
+
+        public PeekTaskRunner( URI path, TransferTransportListener<?> listener )
+        {
+            super( path, listener );
+        }
+
+        protected void runTask()
+            throws Exception
+        {
+            transporter.peek( new PeekTask( path ) );
+        }
+
+    }
+
+    class GetTaskRunner
+        extends TaskRunner
+        implements PartialFile.RemoteAccessChecker, ChecksumValidator.ChecksumFetcher
+    {
+
+        private final File file;
+
+        private final ChecksumValidator checksumValidator;
+
+        public GetTaskRunner( URI path, File file, ChecksumPolicy checksumPolicy,
+                              List<RepositoryLayout.Checksum> checksums, TransferTransportListener<?> listener )
+        {
+            super( path, listener );
+            this.file = requireNonNull( file, "destination file cannot be null" );
+            checksumValidator =
+                new ChecksumValidator( logger, file, fileProcessor, this, checksumPolicy, safe( checksums ) );
+        }
+
+        public void checkRemoteAccess()
+            throws Exception
+        {
+            transporter.peek( new PeekTask( path ) );
+        }
+
+        public boolean fetchChecksum( URI remote, File local )
+            throws Exception
+        {
+            try
+            {
+                transporter.get( new GetTask( remote ).setDataFile( local ) );
+            }
+            catch ( Exception e )
+            {
+                if ( transporter.classify( e ) == Transporter.ERROR_NOT_FOUND )
+                {
+                    return false;
+                }
+                throw e;
+            }
+            return true;
+        }
+
+        protected void runTask()
+            throws Exception
+        {
+            fileProcessor.mkdirs( file.getParentFile() );
+
+            PartialFile partFile = partialFileFactory.newInstance( file, this );
+            if ( partFile == null )
+            {
+                logger.debug( "Concurrent download of " + file + " just finished, skipping download" );
+                return;
+            }
+
+            try
+            {
+                File tmp = partFile.getFile();
+                listener.setChecksumCalculator( checksumValidator.newChecksumCalculator( tmp ) );
+                for ( int firstTrial = 0, lastTrial = 1, trial = firstTrial;; trial++ )
+                {
+                    boolean resume = partFile.isResume() && trial <= firstTrial;
+                    GetTask task = new GetTask( path ).setDataFile( tmp, resume ).setListener( listener );
+                    transporter.get( task );
+                    try
+                    {
+                        checksumValidator.validate( listener.getChecksums(), smartChecksums ? task.getChecksums()
+                                        : null );
+                        break;
+                    }
+                    catch ( ChecksumFailureException e )
+                    {
+                        boolean retry = trial < lastTrial && e.isRetryWorthy();
+                        if ( !retry && !checksumValidator.handle( e ) )
+                        {
+                            throw e;
+                        }
+                        listener.transferCorrupted( e );
+                        if ( retry )
+                        {
+                            checksumValidator.retry();
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                }
+                fileProcessor.move( tmp, file );
+                if ( persistedChecksums )
+                {
+                    checksumValidator.commit();
+                }
+            }
+            finally
+            {
+                partFile.close();
+                checksumValidator.close();
+            }
+        }
+
+    }
+
+    class PutTaskRunner
+        extends TaskRunner
+    {
+
+        private final File file;
+
+        private final Collection<RepositoryLayout.Checksum> checksums;
+
+        public PutTaskRunner( URI path, File file, List<RepositoryLayout.Checksum> checksums,
+                              TransferTransportListener<?> listener )
+        {
+            super( path, listener );
+            this.file = requireNonNull( file, "source file cannot be null" );
+            this.checksums = safe( checksums );
+        }
+
+        protected void runTask()
+            throws Exception
+        {
+            transporter.put( new PutTask( path ).setDataFile( file ).setListener( listener ) );
+            uploadChecksums( file, path );
+        }
+
+        private void uploadChecksums( File file, URI location )
+        {
+            if ( checksums.isEmpty() )
+            {
+                return;
+            }
+            try
+            {
+                Set<String> algos = new HashSet<String>();
+                for ( RepositoryLayout.Checksum checksum : checksums )
+                {
+                    algos.add( checksum.getAlgorithm() );
+                }
+                Map<String, Object> sumsByAlgo = ChecksumUtils.calc( file, algos );
+                for ( RepositoryLayout.Checksum checksum : checksums )
+                {
+                    uploadChecksum( checksum.getLocation(), sumsByAlgo.get( checksum.getAlgorithm() ) );
+                }
+            }
+            catch ( IOException e )
+            {
+                String msg = "Failed to upload checksums for " + file + ": " + e.getMessage();
+                if ( logger.isDebugEnabled() )
+                {
+                    logger.warn( msg, e );
+                }
+                else
+                {
+                    logger.warn( msg );
+                }
+            }
+        }
+
+        private void uploadChecksum( URI location, Object checksum )
+        {
+            try
+            {
+                if ( checksum instanceof Exception )
+                {
+                    throw (Exception) checksum;
+                }
+                transporter.put( new PutTask( location ).setDataString( (String) checksum ) );
+            }
+            catch ( Exception e )
+            {
+                String msg = "Failed to upload checksum " + location + ": " + e.getMessage();
+                if ( logger.isDebugEnabled() )
+                {
+                    logger.warn( msg, e );
+                }
+                else
+                {
+                    logger.warn( msg );
+                }
+            }
+        }
+
+    }
+
+    private static class DirectExecutor
+        implements Executor
+    {
+
+        static final Executor INSTANCE = new DirectExecutor();
+
+        public void execute( Runnable command )
+        {
+            command.run();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java
new file mode 100644
index 0000000..c218744
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java
@@ -0,0 +1,179 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.connector.transport.TransporterProvider;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+
+/**
+ * A repository connector factory that employs pluggable
+ * {@link org.eclipse.aether.spi.connector.transport.TransporterFactory transporters} and
+ * {@link org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory repository layouts} for the transfers.
+ */
+@Named( "basic" )
+public final class BasicRepositoryConnectorFactory
+    implements RepositoryConnectorFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private TransporterProvider transporterProvider;
+
+    private RepositoryLayoutProvider layoutProvider;
+
+    private ChecksumPolicyProvider checksumPolicyProvider;
+
+    private FileProcessor fileProcessor;
+
+    private float priority;
+
+    /**
+     * Creates an (uninitialized) instance of this connector factory. <em>Note:</em> In case of manual instantiation by
+     * clients, the new factory needs to be configured via its various mutators before first use or runtime errors will
+     * occur.
+     */
+    public BasicRepositoryConnectorFactory()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    BasicRepositoryConnectorFactory( TransporterProvider transporterProvider, RepositoryLayoutProvider layoutProvider,
+                                     ChecksumPolicyProvider checksumPolicyProvider, FileProcessor fileProcessor,
+                                     LoggerFactory loggerFactory )
+    {
+        setTransporterProvider( transporterProvider );
+        setRepositoryLayoutProvider( layoutProvider );
+        setChecksumPolicyProvider( checksumPolicyProvider );
+        setFileProcessor( fileProcessor );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setTransporterProvider( locator.getService( TransporterProvider.class ) );
+        setRepositoryLayoutProvider( locator.getService( RepositoryLayoutProvider.class ) );
+        setChecksumPolicyProvider( locator.getService( ChecksumPolicyProvider.class ) );
+        setFileProcessor( locator.getService( FileProcessor.class ) );
+    }
+
+    /**
+     * Sets the logger factory to use for this component.
+     * 
+     * @param loggerFactory The logger factory to use, may be {@code null} to disable logging.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, BasicRepositoryConnector.class );
+        return this;
+    }
+
+    /**
+     * Sets the transporter provider to use for this component.
+     *
+     * @param transporterProvider The transporter provider to use, must not be {@code null}.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setTransporterProvider( TransporterProvider transporterProvider )
+    {
+        this.transporterProvider = requireNonNull( transporterProvider, "transporter provider cannot be null" );
+        return this;
+    }
+
+    /**
+     * Sets the repository layout provider to use for this component.
+     *
+     * @param layoutProvider The repository layout provider to use, must not be {@code null}.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setRepositoryLayoutProvider( RepositoryLayoutProvider layoutProvider )
+    {
+        this.layoutProvider =  requireNonNull( layoutProvider, "repository layout provider cannot be null" );
+        return this;
+    }
+
+    /**
+     * Sets the checksum policy provider to use for this component.
+     *
+     * @param checksumPolicyProvider The checksum policy provider to use, must not be {@code null}.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setChecksumPolicyProvider( ChecksumPolicyProvider checksumPolicyProvider )
+    {
+        this.checksumPolicyProvider = requireNonNull( checksumPolicyProvider, "checksum policy provider cannot be null" );
+        return this;
+    }
+
+    /**
+     * Sets the file processor to use for this component.
+     *
+     * @param fileProcessor The file processor to use, must not be {@code null}.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setFileProcessor( FileProcessor fileProcessor )
+    {
+        this.fileProcessor = requireNonNull( fileProcessor, "file processor cannot be null" );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public BasicRepositoryConnectorFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public RepositoryConnector newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryConnectorException
+    {
+        return new BasicRepositoryConnector( session, repository, transporterProvider, layoutProvider,
+                                             checksumPolicyProvider, fileProcessor, logger );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java
new file mode 100644
index 0000000..db70b01
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java
@@ -0,0 +1,218 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.util.ChecksumUtils;
+
+/**
+ * Calculates checksums for a downloaded file.
+ */
+final class ChecksumCalculator
+{
+
+    static class Checksum
+    {
+        final String algorithm;
+
+        final MessageDigest digest;
+
+        Exception error;
+
+        public Checksum( String algorithm )
+        {
+            this.algorithm = algorithm;
+            MessageDigest digest = null;
+            try
+            {
+                digest = MessageDigest.getInstance( algorithm );
+            }
+            catch ( NoSuchAlgorithmException e )
+            {
+                error = e;
+            }
+            this.digest = digest;
+        }
+
+        public void update( ByteBuffer buffer )
+        {
+            if ( digest != null )
+            {
+                digest.update( buffer );
+            }
+        }
+
+        public void reset()
+        {
+            if ( digest != null )
+            {
+                digest.reset();
+                error = null;
+            }
+        }
+
+        public void error( Exception error )
+        {
+            if ( digest != null )
+            {
+                this.error = error;
+            }
+        }
+
+        public Object get()
+        {
+            if ( error != null )
+            {
+                return error;
+            }
+            return ChecksumUtils.toHexString( digest.digest() );
+        }
+
+    }
+
+    private final List<Checksum> checksums;
+
+    private final File targetFile;
+
+    public static ChecksumCalculator newInstance( File targetFile, Collection<RepositoryLayout.Checksum> checksums )
+    {
+        if ( checksums == null || checksums.isEmpty() )
+        {
+            return null;
+        }
+        return new ChecksumCalculator( targetFile, checksums );
+    }
+
+    private ChecksumCalculator( File targetFile, Collection<RepositoryLayout.Checksum> checksums )
+    {
+        this.checksums = new ArrayList<Checksum>();
+        Set<String> algos = new HashSet<String>();
+        for ( RepositoryLayout.Checksum checksum : checksums )
+        {
+            String algo = checksum.getAlgorithm();
+            if ( algos.add( algo ) )
+            {
+                this.checksums.add( new Checksum( algo ) );
+            }
+        }
+        this.targetFile = targetFile;
+    }
+
+    public void init( long dataOffset )
+    {
+        for ( Checksum checksum : checksums )
+        {
+            checksum.reset();
+        }
+        if ( dataOffset <= 0L )
+        {
+            return;
+        }
+
+        InputStream in = null;
+        try
+        {
+            in = new FileInputStream( targetFile );
+            long total = 0;
+            ByteBuffer buffer = ByteBuffer.allocate( 1024 * 32 );
+            for ( byte[] array = buffer.array(); total < dataOffset; )
+            {
+                int read = in.read( array );
+                if ( read < 0 )
+                {
+                    if ( total < dataOffset )
+                    {
+                        throw new IOException( targetFile + " contains only " + total
+                                                   + " bytes, cannot resume download from offset " + dataOffset );
+                    }
+                    break;
+                }
+                total += read;
+                if ( total > dataOffset )
+                {
+                    read -= total - dataOffset;
+                }
+                buffer.rewind();
+                buffer.limit( read );
+                update( buffer );
+            }
+
+            in.close();
+            in = null;
+        }
+        catch ( IOException e )
+        {
+            for ( Checksum checksum : checksums )
+            {
+                checksum.error( e );
+            }
+        }
+        finally
+        {
+            try
+            {
+                if ( in != null )
+                {
+                    in.close();
+                }
+            }
+            catch ( IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public void update( ByteBuffer data )
+    {
+        for ( Checksum checksum : checksums )
+        {
+            data.mark();
+            checksum.update( data );
+            data.reset();
+        }
+    }
+
+    public Map<String, Object> get()
+    {
+        Map<String, Object> results = new HashMap<String, Object>();
+        for ( Checksum checksum : checksums )
+        {
+            results.put( checksum.algorithm, checksum.get() );
+        }
+        return results;
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
new file mode 100644
index 0000000..8289997
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
@@ -0,0 +1,265 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout.Checksum;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.util.ChecksumUtils;
+
+/**
+ * Performs checksum validation for a downloaded file.
+ */
+final class ChecksumValidator
+{
+
+    interface ChecksumFetcher
+    {
+
+        boolean fetchChecksum( URI remote, File local )
+            throws Exception;
+
+    }
+
+    private final Logger logger;
+
+    private final File dataFile;
+
+    private final Collection<File> tempFiles;
+
+    private final FileProcessor fileProcessor;
+
+    private final ChecksumFetcher checksumFetcher;
+
+    private final ChecksumPolicy checksumPolicy;
+
+    private final Collection<Checksum> checksums;
+
+    private final Map<File, Object> checksumFiles;
+
+    public ChecksumValidator( Logger logger, File dataFile, FileProcessor fileProcessor,
+                              ChecksumFetcher checksumFetcher, ChecksumPolicy checksumPolicy,
+                              Collection<Checksum> checksums )
+    {
+        this.logger = logger;
+        this.dataFile = dataFile;
+        this.tempFiles = new HashSet<File>();
+        this.fileProcessor = fileProcessor;
+        this.checksumFetcher = checksumFetcher;
+        this.checksumPolicy = checksumPolicy;
+        this.checksums = checksums;
+        checksumFiles = new HashMap<File, Object>();
+    }
+
+    public ChecksumCalculator newChecksumCalculator( File targetFile )
+    {
+        if ( checksumPolicy != null )
+        {
+            return ChecksumCalculator.newInstance( targetFile, checksums );
+        }
+        return null;
+    }
+
+    public void validate( Map<String, ?> actualChecksums, Map<String, ?> inlinedChecksums )
+        throws ChecksumFailureException
+    {
+        if ( checksumPolicy == null )
+        {
+            return;
+        }
+        if ( inlinedChecksums != null && validateInlinedChecksums( actualChecksums, inlinedChecksums ) )
+        {
+            return;
+        }
+        if ( validateExternalChecksums( actualChecksums ) )
+        {
+            return;
+        }
+        checksumPolicy.onNoMoreChecksums();
+    }
+
+    private boolean validateInlinedChecksums( Map<String, ?> actualChecksums, Map<String, ?> inlinedChecksums )
+        throws ChecksumFailureException
+    {
+        for ( Map.Entry<String, ?> entry : inlinedChecksums.entrySet() )
+        {
+            String algo = entry.getKey();
+            Object calculated = actualChecksums.get( algo );
+            if ( !( calculated instanceof String ) )
+            {
+                continue;
+            }
+
+            String actual = String.valueOf( calculated );
+            String expected = entry.getValue().toString();
+            checksumFiles.put( getChecksumFile( algo ), expected );
+
+            if ( !isEqualChecksum( expected, actual ) )
+            {
+                checksumPolicy.onChecksumMismatch( algo, ChecksumPolicy.KIND_UNOFFICIAL,
+                                                   new ChecksumFailureException( expected, actual ) );
+            }
+            else if ( checksumPolicy.onChecksumMatch( algo, ChecksumPolicy.KIND_UNOFFICIAL ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean validateExternalChecksums( Map<String, ?> actualChecksums )
+        throws ChecksumFailureException
+    {
+        for ( Checksum checksum : checksums )
+        {
+            String algo = checksum.getAlgorithm();
+            Object calculated = actualChecksums.get( algo );
+            if ( calculated instanceof Exception )
+            {
+                checksumPolicy.onChecksumError( algo, 0, new ChecksumFailureException( (Exception) calculated ) );
+                continue;
+            }
+            try
+            {
+                File checksumFile = getChecksumFile( checksum.getAlgorithm() );
+                File tmp = createTempFile( checksumFile );
+                try
+                {
+                    if ( !checksumFetcher.fetchChecksum( checksum.getLocation(), tmp ) )
+                    {
+                        continue;
+                    }
+                }
+                catch ( Exception e )
+                {
+                    checksumPolicy.onChecksumError( algo, 0, new ChecksumFailureException( e ) );
+                    continue;
+                }
+
+                String actual = String.valueOf( calculated );
+                String expected = ChecksumUtils.read( tmp );
+                checksumFiles.put( checksumFile, tmp );
+
+                if ( !isEqualChecksum( expected, actual ) )
+                {
+                    checksumPolicy.onChecksumMismatch( algo, 0, new ChecksumFailureException( expected, actual ) );
+                }
+                else if ( checksumPolicy.onChecksumMatch( algo, 0 ) )
+                {
+                    return true;
+                }
+            }
+            catch ( IOException e )
+            {
+                checksumPolicy.onChecksumError( algo, 0, new ChecksumFailureException( e ) );
+            }
+        }
+        return false;
+    }
+
+    private static boolean isEqualChecksum( String expected, String actual )
+    {
+        return expected.equalsIgnoreCase( actual );
+    }
+
+    private File getChecksumFile( String algorithm )
+    {
+        String ext = algorithm.replace( "-", "" ).toLowerCase( Locale.ENGLISH );
+        return new File( dataFile.getPath() + '.' + ext );
+    }
+
+    private File createTempFile( File path )
+        throws IOException
+    {
+        File file =
+            File.createTempFile( path.getName() + "-"
+                + UUID.randomUUID().toString().replace( "-", "" ).substring( 0, 8 ), ".tmp", path.getParentFile() );
+        tempFiles.add( file );
+        return file;
+    }
+
+    private void clearTempFiles()
+    {
+        for ( File file : tempFiles )
+        {
+            if ( !file.delete() && file.exists() )
+            {
+                logger.debug( "Could not delete temorary file " + file );
+            }
+        }
+        tempFiles.clear();
+    }
+
+    public void retry()
+    {
+        checksumPolicy.onTransferRetry();
+        checksumFiles.clear();
+        clearTempFiles();
+    }
+
+    public boolean handle( ChecksumFailureException exception )
+    {
+        return checksumPolicy.onTransferChecksumFailure( exception );
+    }
+
+    public void commit()
+    {
+        for ( Map.Entry<File, Object> entry : checksumFiles.entrySet() )
+        {
+            File checksumFile = entry.getKey();
+            Object tmp = entry.getValue();
+            try
+            {
+                if ( tmp instanceof File )
+                {
+                    fileProcessor.move( (File) tmp, checksumFile );
+                    tempFiles.remove( tmp );
+                }
+                else
+                {
+                    fileProcessor.write( checksumFile, String.valueOf( tmp ) );
+                }
+            }
+            catch ( IOException e )
+            {
+                logger.debug( "Failed to write checksum file " + checksumFile + ": " + e.getMessage(), e );
+            }
+        }
+        checksumFiles.clear();
+    }
+
+    public void close()
+    {
+        clearTempFiles();
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/MetadataTransportListener.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/MetadataTransportListener.java
new file mode 100644
index 0000000..7f8bc6d
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/MetadataTransportListener.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.MetadataTransfer;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.TransferEvent;
+
+final class MetadataTransportListener
+    extends TransferTransportListener<MetadataTransfer>
+{
+
+    private final RemoteRepository repository;
+
+    public MetadataTransportListener( MetadataTransfer transfer, RemoteRepository repository,
+                                      TransferEvent.Builder eventBuilder )
+    {
+        super( transfer, eventBuilder );
+        this.repository = repository;
+    }
+
+    @Override
+    public void transferFailed( Exception exception, int classification )
+    {
+        MetadataTransferException e;
+        if ( classification == Transporter.ERROR_NOT_FOUND )
+        {
+            e = new MetadataNotFoundException( getTransfer().getMetadata(), repository );
+        }
+        else
+        {
+            e = new MetadataTransferException( getTransfer().getMetadata(), repository, exception );
+        }
+        getTransfer().setException( e );
+        super.transferFailed( e, classification );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/PartialFile.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/PartialFile.java
new file mode 100644
index 0000000..5a64011
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/PartialFile.java
@@ -0,0 +1,360 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.Channel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.aether.spi.log.Logger;
+
+/**
+ * A partially downloaded file with optional support for resume. If resume is enabled, a well-known location is used for
+ * the partial file in combination with a lock file to prevent concurrent requests from corrupting it (and wasting
+ * network bandwith). Otherwise, a (non-locked) unique temporary file is used.
+ */
+final class PartialFile
+    implements Closeable
+{
+
+    static final String EXT_PART = ".part";
+
+    static final String EXT_LOCK = ".lock";
+
+    interface RemoteAccessChecker
+    {
+
+        void checkRemoteAccess()
+            throws Exception;
+
+    }
+
+    static class LockFile
+    {
+
+        private final File lockFile;
+
+        private final FileLock lock;
+
+        private final AtomicBoolean concurrent;
+
+        public LockFile( File partFile, int requestTimeout, RemoteAccessChecker checker, Logger logger )
+            throws Exception
+        {
+            lockFile = new File( partFile.getPath() + EXT_LOCK );
+            concurrent = new AtomicBoolean( false );
+            lock = lock( lockFile, partFile, requestTimeout, checker, logger, concurrent );
+        }
+
+        private static FileLock lock( File lockFile, File partFile, int requestTimeout, RemoteAccessChecker checker,
+                                      Logger logger, AtomicBoolean concurrent )
+            throws Exception
+        {
+            boolean interrupted = false;
+            try
+            {
+                for ( long lastLength = -1L, lastTime = 0L;; )
+                {
+                    FileLock lock = tryLock( lockFile );
+                    if ( lock != null )
+                    {
+                        return lock;
+                    }
+
+                    long currentLength = partFile.length();
+                    long currentTime = System.currentTimeMillis();
+                    if ( currentLength != lastLength )
+                    {
+                        if ( lastLength < 0L )
+                        {
+                            concurrent.set( true );
+                            /*
+                             * NOTE: We're going with the optimistic assumption that the other thread is downloading the
+                             * file from an equivalent repository. As a bare minimum, ensure the repository we are given
+                             * at least knows about the file and is accessible to us.
+                             */
+                            checker.checkRemoteAccess();
+                            logger.debug( "Concurrent download of " + partFile + " in progress, awaiting completion" );
+                        }
+                        lastLength = currentLength;
+                        lastTime = currentTime;
+                    }
+                    else if ( requestTimeout > 0 && currentTime - lastTime > Math.max( requestTimeout, 3 * 1000 ) )
+                    {
+                        throw new IOException( "Timeout while waiting for concurrent download of " + partFile
+                                                   + " to progress" );
+                    }
+
+                    try
+                    {
+                        Thread.sleep( Math.max( requestTimeout / 2, 100 ) );
+                    }
+                    catch ( InterruptedException e )
+                    {
+                        interrupted = true;
+                    }
+                }
+            }
+            finally
+            {
+                if ( interrupted )
+                {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        private static FileLock tryLock( File lockFile )
+            throws IOException
+        {
+            RandomAccessFile raf = null;
+            FileLock lock = null;
+            try
+            {
+                raf = new RandomAccessFile( lockFile, "rw" );
+                lock = raf.getChannel().tryLock( 0, 1, false );
+
+                if ( lock == null )
+                {
+                    raf.close();
+                    raf = null;
+                }
+            }
+            catch ( OverlappingFileLockException e )
+            {
+                close( raf );
+                raf = null;
+                lock = null;
+            }
+            catch ( RuntimeException e )
+            {
+                close( raf );
+                raf = null;
+                if ( !lockFile.delete() )
+                {
+                    lockFile.deleteOnExit();
+                }
+                throw e;
+            }
+            catch ( IOException e )
+            {
+                close( raf );
+                raf = null;
+                if ( !lockFile.delete() )
+                {
+                    lockFile.deleteOnExit();
+                }
+                throw e;
+            }
+            finally
+            {
+                try
+                {
+                    if ( lock == null && raf != null )
+                    {
+                        raf.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+
+            return lock;
+        }
+
+        private static void close( Closeable file )
+        {
+            try
+            {
+                if ( file != null )
+                {
+                    file.close();
+                }
+            }
+            catch ( IOException e )
+            {
+                // Suppressed.
+            }
+        }
+
+        public boolean isConcurrent()
+        {
+            return concurrent.get();
+        }
+
+        public void close() throws IOException
+        {
+            Channel channel = null;
+            try
+            {
+                channel = lock.channel();
+                lock.release();
+                channel.close();
+                channel = null;
+            }
+            finally
+            {
+                try
+                {
+                    if ( channel != null )
+                    {
+                        channel.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+                finally
+                {
+                    if ( !lockFile.delete() )
+                    {
+                        lockFile.deleteOnExit();
+                    }
+                }
+            }
+        }
+
+        @Override
+        public String toString()
+        {
+            return lockFile + " - " + lock.isValid();
+        }
+
+    }
+
+    static class Factory
+    {
+
+        private final boolean resume;
+
+        private final long resumeThreshold;
+
+        private final int requestTimeout;
+
+        private final Logger logger;
+
+        public Factory( boolean resume, long resumeThreshold, int requestTimeout, Logger logger )
+        {
+            this.resume = resume;
+            this.resumeThreshold = resumeThreshold;
+            this.requestTimeout = requestTimeout;
+            this.logger = logger;
+        }
+
+        public PartialFile newInstance( File dstFile, RemoteAccessChecker checker )
+            throws Exception
+        {
+            if ( resume )
+            {
+                File partFile = new File( dstFile.getPath() + EXT_PART );
+
+                long reqTimestamp = System.currentTimeMillis();
+                LockFile lockFile = new LockFile( partFile, requestTimeout, checker, logger );
+                if ( lockFile.isConcurrent() && dstFile.lastModified() >= reqTimestamp - 100L )
+                {
+                    lockFile.close();
+                    return null;
+                }
+                try
+                {
+                    if ( !partFile.createNewFile() && !partFile.isFile() )
+                    {
+                        throw new IOException( partFile.exists() ? "Path exists but is not a file" : "Unknown error" );
+                    }
+                    return new PartialFile( partFile, lockFile, resumeThreshold, logger );
+                }
+                catch ( IOException e )
+                {
+                    lockFile.close();
+                    logger.debug( "Cannot create resumable file " + partFile.getAbsolutePath() + ": " + e );
+                    // fall through and try non-resumable/temporary file location
+                }
+            }
+
+            File tempFile =
+                File.createTempFile( dstFile.getName() + '-' + UUID.randomUUID().toString().replace( "-", "" ), ".tmp",
+                                     dstFile.getParentFile() );
+            return new PartialFile( tempFile, logger );
+        }
+
+    }
+
+    private final File partFile;
+
+    private final LockFile lockFile;
+
+    private final long threshold;
+
+    private final Logger logger;
+
+    private PartialFile( File partFile, Logger logger )
+    {
+        this( partFile, null, 0L, logger );
+    }
+
+    private PartialFile( File partFile, LockFile lockFile, long threshold, Logger logger )
+    {
+        this.partFile = partFile;
+        this.lockFile = lockFile;
+        this.threshold = threshold;
+        this.logger = logger;
+    }
+
+    public File getFile()
+    {
+        return partFile;
+    }
+
+    public boolean isResume()
+    {
+        return lockFile != null && partFile.length() >= threshold;
+    }
+
+    public void close() throws IOException
+    {
+        if ( partFile.exists() && !isResume() )
+        {
+            if ( !partFile.delete() && partFile.exists() )
+            {
+                logger.debug( "Could not delete temorary file " + partFile );
+            }
+        }
+        if ( lockFile != null )
+        {
+            lockFile.close();
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getFile() );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/TransferTransportListener.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/TransferTransportListener.java
new file mode 100644
index 0000000..bd95577
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/TransferTransportListener.java
@@ -0,0 +1,141 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Map;
+
+import org.eclipse.aether.spi.connector.Transfer;
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferEvent.EventType;
+import org.eclipse.aether.transfer.TransferListener;
+
+class TransferTransportListener<T extends Transfer>
+    extends TransportListener
+{
+
+    private final T transfer;
+
+    private final TransferListener listener;
+
+    private final TransferEvent.Builder eventBuilder;
+
+    private ChecksumCalculator checksumCalculator;
+
+    protected TransferTransportListener( T transfer, TransferEvent.Builder eventBuilder )
+    {
+        this.transfer = transfer;
+        this.listener = transfer.getListener();
+        this.eventBuilder = eventBuilder;
+    }
+
+    protected T getTransfer()
+    {
+        return transfer;
+    }
+
+    public void transferInitiated()
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.INITIATED );
+            listener.transferInitiated( eventBuilder.build() );
+        }
+    }
+
+    @Override
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+        if ( checksumCalculator != null )
+        {
+            checksumCalculator.init( dataOffset );
+        }
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.STARTED ).setTransferredBytes( dataOffset );
+            TransferEvent event = eventBuilder.build();
+            event.getResource().setContentLength( dataLength ).setResumeOffset( dataOffset );
+            listener.transferStarted( event );
+        }
+    }
+
+    @Override
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+        if ( checksumCalculator != null )
+        {
+            checksumCalculator.update( data );
+        }
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.PROGRESSED ).addTransferredBytes( data.remaining() ).setDataBuffer( data );
+            listener.transferProgressed( eventBuilder.build() );
+        }
+    }
+
+    public void transferCorrupted( Exception exception )
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.CORRUPTED ).setException( exception );
+            listener.transferCorrupted( eventBuilder.build() );
+        }
+    }
+
+    public void transferFailed( Exception exception, int classification )
+    {
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.FAILED ).setException( exception );
+            listener.transferFailed( eventBuilder.build() );
+        }
+    }
+
+    public void transferSucceeded()
+    {
+        if ( listener != null )
+        {
+            eventBuilder.resetType( EventType.SUCCEEDED );
+            listener.transferSucceeded( eventBuilder.build() );
+        }
+    }
+
+    public Map<String, Object> getChecksums()
+    {
+        if ( checksumCalculator == null )
+        {
+            return Collections.emptyMap();
+        }
+        return checksumCalculator.get();
+    }
+
+    public void setChecksumCalculator( ChecksumCalculator checksumCalculator )
+    {
+        this.checksumCalculator = checksumCalculator;
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/package-info.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/package-info.java
new file mode 100644
index 0000000..df86897
--- /dev/null
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Support for downloads/uploads using remote repositories that have a URI-based content structure/layout.
+ */
+package org.eclipse.aether.connector.basic;
+
diff --git a/maven-resolver-connector-basic/src/site/site.xml b/maven-resolver-connector-basic/src/site/site.xml
new file mode 100644
index 0000000..25a0b9d
--- /dev/null
+++ b/maven-resolver-connector-basic/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Connector Basic">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java
new file mode 100644
index 0000000..993f94d
--- /dev/null
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java
@@ -0,0 +1,163 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChecksumCalculatorTest
+{
+
+    private static final String SHA1 = "SHA-1";
+
+    private static final String MD5 = "MD5";
+
+    private File file;
+
+    private ChecksumCalculator newCalculator( String... algos )
+    {
+        List<RepositoryLayout.Checksum> checksums = new ArrayList<RepositoryLayout.Checksum>();
+        for ( String algo : algos )
+        {
+            checksums.add( new RepositoryLayout.Checksum( algo, URI.create( "irrelevant" ) ) );
+        }
+        return ChecksumCalculator.newInstance( file, checksums );
+    }
+
+    private ByteBuffer toBuffer( String data )
+    {
+        return ByteBuffer.wrap( data.getBytes( StandardCharsets.UTF_8 ) );
+    }
+
+    @Before
+    public void init()
+        throws Exception
+    {
+        file = TestFileUtils.createTempFile( "Hello World!" );
+    }
+
+    @Test
+    public void testNoOffset()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.init( 0 );
+        calculator.update( toBuffer( "Hello World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertEquals( "ed076287532e86365e841e92bfc50d8c", digests.get( MD5 ) );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testWithOffset()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.init( 6 );
+        calculator.update( toBuffer( "World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertEquals( "ed076287532e86365e841e92bfc50d8c", digests.get( MD5 ) );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testWithExcessiveOffset()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.init( 100 );
+        calculator.update( toBuffer( "World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertTrue( digests.get( SHA1 ) instanceof IOException );
+        assertTrue( digests.get( MD5 ) instanceof IOException );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testUnknownAlgorithm()
+    {
+        ChecksumCalculator calculator = newCalculator( "unknown", SHA1 );
+        calculator.init( 0 );
+        calculator.update( toBuffer( "Hello World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertTrue( digests.get( "unknown" ) instanceof NoSuchAlgorithmException );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testNoInitCall()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.update( toBuffer( "Hello World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertEquals( "ed076287532e86365e841e92bfc50d8c", digests.get( MD5 ) );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testRestart()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.init( 0 );
+        calculator.update( toBuffer( "Ignored" ) );
+        calculator.init( 0 );
+        calculator.update( toBuffer( "Hello World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertEquals( "ed076287532e86365e841e92bfc50d8c", digests.get( MD5 ) );
+        assertEquals( 2, digests.size() );
+    }
+
+    @Test
+    public void testRestartAfterError()
+    {
+        ChecksumCalculator calculator = newCalculator( SHA1, MD5 );
+        calculator.init( 100 );
+        calculator.init( 0 );
+        calculator.update( toBuffer( "Hello World!" ) );
+        Map<String, Object> digests = calculator.get();
+        assertNotNull( digests );
+        assertEquals( "2ef7bde608ce5404e97d5f042f95f89f1c232871", digests.get( SHA1 ) );
+        assertEquals( "ed076287532e86365e841e92bfc50d8c", digests.get( MD5 ) );
+        assertEquals( 2, digests.size() );
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
new file mode 100644
index 0000000..6d67768
--- /dev/null
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
@@ -0,0 +1,465 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.internal.test.util.TestFileProcessor;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChecksumValidatorTest
+{
+
+    private static class StubChecksumPolicy
+        implements ChecksumPolicy
+    {
+
+        boolean inspectAll;
+
+        boolean tolerateFailure;
+
+        private List<String> callbacks = new ArrayList<String>();
+
+        private Object conclusion;
+
+        public boolean onChecksumMatch( String algorithm, int kind )
+        {
+            callbacks.add( String.format( "match(%s, %04x)", algorithm, kind ) );
+            if ( inspectAll )
+            {
+                if ( conclusion == null )
+                {
+                    conclusion = true;
+                }
+                return false;
+            }
+            return true;
+        }
+
+        public void onChecksumMismatch( String algorithm, int kind, ChecksumFailureException exception )
+            throws ChecksumFailureException
+        {
+            callbacks.add( String.format( "mismatch(%s, %04x)", algorithm, kind ) );
+            if ( inspectAll )
+            {
+                conclusion = exception;
+                return;
+            }
+            throw exception;
+        }
+
+        public void onChecksumError( String algorithm, int kind, ChecksumFailureException exception )
+            throws ChecksumFailureException
+        {
+            callbacks.add( String.format( "error(%s, %04x, %s)", algorithm, kind, exception.getCause().getMessage() ) );
+        }
+
+        public void onNoMoreChecksums()
+            throws ChecksumFailureException
+        {
+            callbacks.add( String.format( "noMore()" ) );
+            if ( conclusion instanceof ChecksumFailureException )
+            {
+                throw (ChecksumFailureException) conclusion;
+            }
+            else if ( !Boolean.TRUE.equals( conclusion ) )
+            {
+                throw new ChecksumFailureException( "no checksums" );
+            }
+        }
+
+        public void onTransferRetry()
+        {
+            callbacks.add( String.format( "retry()" ) );
+        }
+
+        public boolean onTransferChecksumFailure( ChecksumFailureException exception )
+        {
+            callbacks.add( String.format( "fail(%s)", exception.getMessage() ) );
+            return tolerateFailure;
+        }
+
+        void assertCallbacks( String... callbacks )
+        {
+            assertEquals( Arrays.asList( callbacks ), this.callbacks );
+        }
+
+    }
+
+    private static class StubChecksumFetcher
+        implements ChecksumValidator.ChecksumFetcher
+    {
+
+        Map<URI, Object> checksums = new HashMap<URI, Object>();
+
+        List<File> checksumFiles = new ArrayList<File>();
+
+        private List<URI> fetchedFiles = new ArrayList<URI>();
+
+        public boolean fetchChecksum( URI remote, File local )
+            throws Exception
+        {
+            fetchedFiles.add( remote );
+            Object checksum = checksums.get( remote );
+            if ( checksum == null )
+            {
+                return false;
+            }
+            if ( checksum instanceof Exception )
+            {
+                throw (Exception) checksum;
+            }
+            TestFileUtils.writeString( local, checksum.toString() );
+            checksumFiles.add( local );
+            return true;
+        }
+
+        void mock( String algo, Object value )
+        {
+            checksums.put( toUri( algo ), value );
+        }
+
+        void assertFetchedFiles( String... algos )
+        {
+            List<URI> expected = new ArrayList<URI>();
+            for ( String algo : algos )
+            {
+                expected.add( toUri( algo ) );
+            }
+            assertEquals( expected, fetchedFiles );
+        }
+
+        private static URI toUri( String algo )
+        {
+            return newChecksum( algo ).getLocation();
+        }
+
+    }
+
+    private static final String SHA1 = "SHA-1";
+
+    private static final String MD5 = "MD5";
+
+    private StubChecksumPolicy policy;
+
+    private StubChecksumFetcher fetcher;
+
+    private File dataFile;
+
+    private static RepositoryLayout.Checksum newChecksum( String algo )
+    {
+        return RepositoryLayout.Checksum.forLocation( URI.create( "file" ), algo );
+    }
+
+    private List<RepositoryLayout.Checksum> newChecksums( String... algos )
+    {
+        List<RepositoryLayout.Checksum> checksums = new ArrayList<RepositoryLayout.Checksum>();
+        for ( String algo : algos )
+        {
+            checksums.add( newChecksum( algo ) );
+        }
+        return checksums;
+    }
+
+    private ChecksumValidator newValidator( String... algos )
+    {
+        return new ChecksumValidator( new TestLoggerFactory().getLogger( "" ), dataFile, new TestFileProcessor(),
+                                      fetcher, policy, newChecksums( algos ) );
+    }
+
+    private Map<String, ?> checksums( String... algoDigestPairs )
+    {
+        Map<String, Object> checksums = new LinkedHashMap<String, Object>();
+        for ( int i = 0; i < algoDigestPairs.length; i += 2 )
+        {
+            String algo = algoDigestPairs[i];
+            String digest = algoDigestPairs[i + 1];
+            if ( digest == null )
+            {
+                checksums.put( algo, new IOException( "error" ) );
+            }
+            else
+            {
+                checksums.put( algo, digest );
+            }
+        }
+        return checksums;
+    }
+
+    @Before
+    public void init()
+        throws Exception
+    {
+        dataFile = TestFileUtils.createTempFile( "" );
+        dataFile.delete();
+        policy = new StubChecksumPolicy();
+        fetcher = new StubChecksumFetcher();
+    }
+
+    @Test
+    public void testValidate_NullPolicy()
+        throws Exception
+    {
+        policy = null;
+        ChecksumValidator validator = newValidator( SHA1 );
+        validator.validate( checksums( SHA1, "ignored" ), null );
+        fetcher.assertFetchedFiles();
+    }
+
+    @Test
+    public void testValidate_AcceptOnFirstMatch()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1 );
+        fetcher.mock( SHA1, "foo" );
+        validator.validate( checksums( SHA1, "foo" ), null );
+        fetcher.assertFetchedFiles( SHA1 );
+        policy.assertCallbacks( "match(SHA-1, 0000)" );
+    }
+
+    @Test
+    public void testValidate_FailOnFirstMismatch()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1 );
+        fetcher.mock( SHA1, "foo" );
+        try
+        {
+            validator.validate( checksums( SHA1, "not-foo" ), null );
+            fail( "expected exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertEquals( "foo", e.getExpected() );
+            assertEquals( "not-foo", e.getActual() );
+            assertTrue( e.isRetryWorthy() );
+        }
+        fetcher.assertFetchedFiles( SHA1 );
+        policy.assertCallbacks( "mismatch(SHA-1, 0000)" );
+    }
+
+    @Test
+    public void testValidate_AcceptOnEnd()
+        throws Exception
+    {
+        policy.inspectAll = true;
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( SHA1, "foo" );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( SHA1, "foo", MD5, "bar" ), null );
+        fetcher.assertFetchedFiles( SHA1, MD5 );
+        policy.assertCallbacks( "match(SHA-1, 0000)", "match(MD5, 0000)", "noMore()" );
+    }
+
+    @Test
+    public void testValidate_FailOnEnd()
+        throws Exception
+    {
+        policy.inspectAll = true;
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( SHA1, "foo" );
+        fetcher.mock( MD5, "bar" );
+        try
+        {
+            validator.validate( checksums( SHA1, "not-foo", MD5, "bar" ), null );
+            fail( "expected exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertEquals( "foo", e.getExpected() );
+            assertEquals( "not-foo", e.getActual() );
+            assertTrue( e.isRetryWorthy() );
+        }
+        fetcher.assertFetchedFiles( SHA1, MD5 );
+        policy.assertCallbacks( "mismatch(SHA-1, 0000)", "match(MD5, 0000)", "noMore()" );
+    }
+
+    @Test
+    public void testValidate_InlinedBeforeExternal()
+        throws Exception
+    {
+        policy.inspectAll = true;
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( SHA1, "foo" );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( SHA1, "foo", MD5, "bar" ), checksums( SHA1, "foo", MD5, "bar" ) );
+        fetcher.assertFetchedFiles( SHA1, MD5 );
+        policy.assertCallbacks( "match(SHA-1, 0001)", "match(MD5, 0001)", "match(SHA-1, 0000)", "match(MD5, 0000)",
+                                "noMore()" );
+    }
+
+    @Test
+    public void testValidate_CaseInsensitive()
+        throws Exception
+    {
+        policy.inspectAll = true;
+        ChecksumValidator validator = newValidator( SHA1 );
+        fetcher.mock( SHA1, "FOO" );
+        validator.validate( checksums( SHA1, "foo" ), checksums( SHA1, "foo" ) );
+        policy.assertCallbacks( "match(SHA-1, 0001)", "match(SHA-1, 0000)", "noMore()" );
+    }
+
+    @Test
+    public void testValidate_MissingRemoteChecksum()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( MD5, "bar" ), null );
+        fetcher.assertFetchedFiles( SHA1, MD5 );
+        policy.assertCallbacks( "match(MD5, 0000)" );
+    }
+
+    @Test
+    public void testValidate_InaccessibleRemoteChecksum()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( SHA1, new IOException( "inaccessible" ) );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( MD5, "bar" ), null );
+        fetcher.assertFetchedFiles( SHA1, MD5 );
+        policy.assertCallbacks( "error(SHA-1, 0000, inaccessible)", "match(MD5, 0000)" );
+    }
+
+    @Test
+    public void testValidate_InaccessibleLocalChecksum()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( SHA1, "foo" );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( SHA1, null, MD5, "bar" ), null );
+        fetcher.assertFetchedFiles( MD5 );
+        policy.assertCallbacks( "error(SHA-1, 0000, error)", "match(MD5, 0000)" );
+    }
+
+    @Test
+    public void testHandle_Accept()
+        throws Exception
+    {
+        policy.tolerateFailure = true;
+        ChecksumValidator validator = newValidator( SHA1 );
+        assertEquals( true, validator.handle( new ChecksumFailureException( "accept" ) ) );
+        policy.assertCallbacks( "fail(accept)" );
+    }
+
+    @Test
+    public void testHandle_Reject()
+        throws Exception
+    {
+        policy.tolerateFailure = false;
+        ChecksumValidator validator = newValidator( SHA1 );
+        assertEquals( false, validator.handle( new ChecksumFailureException( "reject" ) ) );
+        policy.assertCallbacks( "fail(reject)" );
+    }
+
+    @Test
+    public void testRetry_ResetPolicy()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1 );
+        validator.retry();
+        policy.assertCallbacks( "retry()" );
+    }
+
+    @Test
+    public void testRetry_RemoveTempFiles()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1 );
+        fetcher.mock( SHA1, "foo" );
+        validator.validate( checksums( SHA1, "foo" ), null );
+        fetcher.assertFetchedFiles( SHA1 );
+        assertEquals( 1, fetcher.checksumFiles.size() );
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertTrue( file.getAbsolutePath(), file.isFile() );
+        }
+        validator.retry();
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertFalse( file.getAbsolutePath(), file.exists() );
+        }
+    }
+
+    @Test
+    public void testCommit_SaveChecksumFiles()
+        throws Exception
+    {
+        policy.inspectAll = true;
+        ChecksumValidator validator = newValidator( SHA1, MD5 );
+        fetcher.mock( MD5, "bar" );
+        validator.validate( checksums( SHA1, "foo", MD5, "bar" ), checksums( SHA1, "foo" ) );
+        assertEquals( 1, fetcher.checksumFiles.size() );
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertTrue( file.getAbsolutePath(), file.isFile() );
+        }
+        validator.commit();
+        File checksumFile = new File( dataFile.getPath() + ".sha1" );
+        assertTrue( checksumFile.getAbsolutePath(), checksumFile.isFile() );
+        assertEquals( "foo", TestFileUtils.readString( checksumFile ) );
+        checksumFile = new File( dataFile.getPath() + ".md5" );
+        assertTrue( checksumFile.getAbsolutePath(), checksumFile.isFile() );
+        assertEquals( "bar", TestFileUtils.readString( checksumFile ) );
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertFalse( file.getAbsolutePath(), file.exists() );
+        }
+    }
+
+    @Test
+    public void testClose_RemoveTempFiles()
+        throws Exception
+    {
+        ChecksumValidator validator = newValidator( SHA1 );
+        fetcher.mock( SHA1, "foo" );
+        validator.validate( checksums( SHA1, "foo" ), null );
+        fetcher.assertFetchedFiles( SHA1 );
+        assertEquals( 1, fetcher.checksumFiles.size() );
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertTrue( file.getAbsolutePath(), file.isFile() );
+        }
+        validator.close();
+        for ( File file : fetcher.checksumFiles )
+        {
+            assertFalse( file.getAbsolutePath(), file.exists() );
+        }
+    }
+
+}
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/PartialFileTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/PartialFileTest.java
new file mode 100644
index 0000000..61a83a0
--- /dev/null
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/PartialFileTest.java
@@ -0,0 +1,371 @@
+package org.eclipse.aether.connector.basic;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileLock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PartialFileTest
+{
+
+    private static class StubRemoteAccessChecker
+        implements PartialFile.RemoteAccessChecker
+    {
+
+        Exception exception;
+
+        int invocations;
+
+        public void checkRemoteAccess()
+            throws Exception
+        {
+            invocations++;
+            if ( exception != null )
+            {
+                throw exception;
+            }
+        }
+
+    }
+
+    private static class ConcurrentWriter
+        extends Thread
+    {
+
+        private final File dstFile;
+
+        private final File partFile;
+
+        private final File lockFile;
+
+        private final CountDownLatch locked;
+
+        private final int sleep;
+
+        volatile int length;
+
+        Exception error;
+
+        public ConcurrentWriter( File dstFile, int sleep, int length )
+            throws InterruptedException
+        {
+            super( "ConcurrentWriter-" + dstFile.getAbsolutePath() );
+            this.dstFile = dstFile;
+            partFile = new File( dstFile.getPath() + PartialFile.EXT_PART );
+            lockFile = new File( partFile.getPath() + PartialFile.EXT_LOCK );
+            this.sleep = sleep;
+            this.length = length;
+            locked = new CountDownLatch( 1 );
+            start();
+            locked.await();
+        }
+
+        @Override
+        public void run()
+        {
+            RandomAccessFile raf = null;
+            FileLock lock = null;
+            OutputStream out = null;
+            try
+            {
+                raf = new RandomAccessFile( lockFile, "rw" );
+                lock = raf.getChannel().lock( 0, 1, false );
+                locked.countDown();
+                out = new FileOutputStream( partFile );
+                for ( int i = 0, n = Math.abs( length ); i < n; i++ )
+                {
+                    for ( long start = System.currentTimeMillis(); System.currentTimeMillis() - start < sleep; )
+                    {
+                        Thread.sleep( 10 );
+                    }
+                    out.write( 65 );
+                    out.flush();
+                    System.out.println( "  " + System.currentTimeMillis() + " Wrote byte " + ( i + 1 ) + "/"
+                                            + n );
+                }
+                if ( length >= 0 && !dstFile.setLastModified( System.currentTimeMillis() ) )
+                {
+                    throw new IOException( "Could not update destination file" );
+                }
+
+                out.close();
+                out = null;
+                lock.release();
+                lock = null;
+                raf.close();
+                raf = null;
+            }
+            catch ( Exception e )
+            {
+                error = e;
+            }
+            finally
+            {
+                try
+                {
+                    if ( out != null )
+                    {
+                        out.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( lock != null )
+                        {
+                            lock.release();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                    finally
+                    {
+                        try
+                        {
+                            if ( raf != null )
+                            {
+                                raf.close();
+                            }
+                        }
+                        catch ( final IOException e )
+                        {
+                            // Suppressed due to an exception already thrown in the try block.
+                        }
+                        finally
+                        {
+                            if ( !lockFile.delete() )
+                            {
+                                lockFile.deleteOnExit();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+    }
+
+    private static final boolean PROPER_LOCK_SUPPORT;
+
+    static
+    {
+        String javaVersion = System.getProperty( "java.version" ).trim();
+        boolean notJava5 = !javaVersion.startsWith( "1.5." );
+        String osName = System.getProperty( "os.name" ).toLowerCase( Locale.ENGLISH );
+        boolean windows = osName.contains( "windows" );
+        PROPER_LOCK_SUPPORT = notJava5 || windows;
+    }
+
+    private StubRemoteAccessChecker remoteAccessChecker;
+
+    private File dstFile;
+
+    private File partFile;
+
+    private File lockFile;
+
+    private List<Closeable> closeables;
+
+    private PartialFile newPartialFile( long resumeThreshold, int requestTimeout )
+        throws Exception
+    {
+        PartialFile.Factory factory =
+            new PartialFile.Factory( resumeThreshold >= 0L, resumeThreshold, requestTimeout,
+                                     new TestLoggerFactory().getLogger( "" ) );
+        PartialFile partFile = factory.newInstance( dstFile, remoteAccessChecker );
+        if ( partFile != null )
+        {
+            closeables.add( partFile );
+        }
+        return partFile;
+    }
+
+    @Before
+    public void init()
+        throws Exception
+    {
+        closeables = new ArrayList<Closeable>();
+        remoteAccessChecker = new StubRemoteAccessChecker();
+        dstFile = TestFileUtils.createTempFile( "Hello World!" );
+        partFile = new File( dstFile.getPath() + PartialFile.EXT_PART );
+        lockFile = new File( partFile.getPath() + PartialFile.EXT_LOCK );
+    }
+
+    @After
+    public void exit()
+    {
+        for ( Closeable closeable : closeables )
+        {
+            try
+            {
+                closeable.close();
+            }
+            catch ( Exception e )
+            {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    @Test
+    public void testCloseNonResumableFile()
+        throws Exception
+    {
+        PartialFile partialFile = newPartialFile( -1, 100 );
+        assertNotNull( partialFile );
+        assertNotNull( partialFile.getFile() );
+        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
+        partialFile.close();
+        assertFalse( partialFile.getFile().getAbsolutePath(), partialFile.getFile().exists() );
+    }
+
+    @Test
+    public void testCloseResumableFile()
+        throws Exception
+    {
+        PartialFile partialFile = newPartialFile( 0, 100 );
+        assertNotNull( partialFile );
+        assertNotNull( partialFile.getFile() );
+        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
+        assertEquals( partFile, partialFile.getFile() );
+        assertTrue( lockFile.getAbsolutePath(), lockFile.isFile() );
+        partialFile.close();
+        assertTrue( partialFile.getFile().getAbsolutePath(), partialFile.getFile().isFile() );
+        assertFalse( lockFile.getAbsolutePath(), lockFile.exists() );
+    }
+
+    @Test
+    public void testResumableFileCreationError()
+        throws Exception
+    {
+        assertTrue( partFile.getAbsolutePath(), partFile.mkdirs() );
+        PartialFile partialFile = newPartialFile( 0, 100 );
+        assertNotNull( partialFile );
+        assertFalse( partialFile.isResume() );
+        assertFalse( lockFile.getAbsolutePath(), lockFile.exists() );
+    }
+
+    @Test
+    public void testResumeThreshold()
+        throws Exception
+    {
+        PartialFile partialFile = newPartialFile( 0, 100 );
+        assertNotNull( partialFile );
+        assertTrue( partialFile.isResume() );
+        partialFile.close();
+        partialFile = newPartialFile( 1, 100 );
+        assertNotNull( partialFile );
+        assertFalse( partialFile.isResume() );
+        partialFile.close();
+    }
+
+    @Test( timeout = 10000L )
+    public void testResumeConcurrently_RequestTimeout()
+        throws Exception
+    {
+        assumeTrue( PROPER_LOCK_SUPPORT );
+        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 5 * 1000, 1 );
+        try
+        {
+            newPartialFile( 0, 1000 );
+            fail( "expected exception" );
+        }
+        catch ( Exception e )
+        {
+            assertTrue( e.getMessage().contains( "Timeout" ) );
+        }
+        writer.interrupt();
+        writer.join();
+    }
+
+    @Test( timeout = 10000L )
+    public void testResumeConcurrently_AwaitCompletion_ConcurrentWriterSucceeds()
+        throws Exception
+    {
+        assumeTrue( PROPER_LOCK_SUPPORT );
+        assertTrue( dstFile.setLastModified( System.currentTimeMillis() - 60L * 1000L ) );
+        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 100, 10 );
+        assertNull( newPartialFile( 0, 500 ) );
+        writer.join();
+        assertNull( writer.error );
+        assertEquals( 1, remoteAccessChecker.invocations );
+    }
+
+    @Test( timeout = 10000L )
+    public void testResumeConcurrently_AwaitCompletion_ConcurrentWriterFails()
+        throws Exception
+    {
+        assumeTrue( PROPER_LOCK_SUPPORT );
+        assertTrue( dstFile.setLastModified( System.currentTimeMillis() - 60L * 1000L ) );
+        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 100, -10 );
+        PartialFile partialFile = newPartialFile( 0, 500 );
+        assertNotNull( partialFile );
+        assertTrue( partialFile.isResume() );
+        writer.join();
+        assertNull( writer.error );
+        assertEquals( 1, remoteAccessChecker.invocations );
+    }
+
+    @Test( timeout = 10000L )
+    public void testResumeConcurrently_CheckRemoteAccess()
+        throws Exception
+    {
+        assumeTrue( PROPER_LOCK_SUPPORT );
+        remoteAccessChecker.exception = new IOException( "missing" );
+        ConcurrentWriter writer = new ConcurrentWriter( dstFile, 1000, 1 );
+        try
+        {
+            newPartialFile( 0, 1000 );
+            fail( "expected exception" );
+        }
+        catch ( Exception e )
+        {
+            assertSame( remoteAccessChecker.exception, e );
+        }
+        writer.interrupt();
+        writer.join();
+    }
+
+}
diff --git a/maven-resolver-impl/pom.xml b/maven-resolver-impl/pom.xml
new file mode 100644
index 0000000..acef976
--- /dev/null
+++ b/maven-resolver-impl/pom.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-impl</artifactId>
+
+  <name>Maven Artifact Resolver Implementation</name>
+  <description>
+    An implementation of the repository system.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.impl</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.sisu</groupId>
+      <artifactId>org.eclipse.sisu.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/AetherModule.java
new file mode 100644
index 0000000..4e05ec2
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/AetherModule.java
@@ -0,0 +1,35 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+/**
+ * A ready-made Guice module that sets up bindings for all components from this library. To acquire a complete
+ * repository system, clients need to bind an artifact descriptor reader, a version resolver, a version range resolver,
+ * zero or more metadata generator factories, some repository connector and transporter factories to access remote
+ * repositories.
+ * 
+ * @deprecated Use {@link org.eclipse.aether.impl.guice.AetherModule} instead.
+ */
+@Deprecated
+public final class AetherModule
+    extends org.eclipse.aether.impl.guice.AetherModule
+{
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactDescriptorReader.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactDescriptorReader.java
new file mode 100644
index 0000000..66f3528
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactDescriptorReader.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+
+/**
+ * Provides information about an artifact that is relevant to transitive dependency resolution. Each artifact is expected
+ * to have an accompanying <em>artifact descriptor</em> that among others lists the direct dependencies of the artifact.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface ArtifactDescriptorReader
+{
+
+    /**
+     * Gets information about an artifact like its direct dependencies and potential relocations. Implementations must
+     * respect the {@link RepositorySystemSession#getArtifactDescriptorPolicy() artifact descriptor policy} of the
+     * session when dealing with certain error cases.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The descriptor request, must not be {@code null}
+     * @return The descriptor result, never {@code null}.
+     * @throws ArtifactDescriptorException If the artifact descriptor could not be read.
+     * @see RepositorySystem#readArtifactDescriptor(RepositorySystemSession, ArtifactDescriptorRequest)
+     */
+    ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session, ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactResolver.java
new file mode 100644
index 0000000..3b43592
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/ArtifactResolver.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+
+/**
+ * Resolves artifacts, that is gets a local filesystem path to their binary contents.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface ArtifactResolver
+{
+
+    /**
+     * Resolves the path for an artifact. The artifact will be downloaded to the local repository if necessary. An
+     * artifact that is already resolved will be skipped and is not re-resolved. Note that this method assumes that any
+     * relocations have already been processed and the artifact coordinates are used as-is.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The resolution request, must not be {@code null}.
+     * @return The resolution result, never {@code null}.
+     * @throws ArtifactResolutionException If the artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see RepositorySystem#resolveArtifact(RepositorySystemSession, ArtifactRequest)
+     */
+    ArtifactResult resolveArtifact( RepositorySystemSession session, ArtifactRequest request )
+        throws ArtifactResolutionException;
+
+    /**
+     * Resolves the paths for a collection of artifacts. Artifacts will be downloaded to the local repository if
+     * necessary. Artifacts that are already resolved will be skipped and are not re-resolved. Note that this method
+     * assumes that any relocations have already been processed and the artifact coordinates are used as-is.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @throws ArtifactResolutionException If any artifact could not be resolved.
+     * @see Artifact#getFile()
+     * @see RepositorySystem#resolveArtifacts(RepositorySystemSession, Collection)
+     */
+    List<ArtifactResult> resolveArtifacts( RepositorySystemSession session,
+                                           Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
new file mode 100644
index 0000000..52ae3c2
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
@@ -0,0 +1,332 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
+import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
+import org.eclipse.aether.internal.impl.DefaultDependencyCollector;
+import org.eclipse.aether.internal.impl.DefaultDeployer;
+import org.eclipse.aether.internal.impl.DefaultFileProcessor;
+import org.eclipse.aether.internal.impl.DefaultInstaller;
+import org.eclipse.aether.internal.impl.DefaultLocalRepositoryProvider;
+import org.eclipse.aether.internal.impl.DefaultMetadataResolver;
+import org.eclipse.aether.internal.impl.DefaultOfflineController;
+import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
+import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
+import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
+import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
+import org.eclipse.aether.internal.impl.DefaultSyncContextFactory;
+import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
+import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
+import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
+import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManagerFactory;
+import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
+import org.eclipse.aether.internal.impl.slf4j.Slf4jLoggerFactory;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.connector.transport.TransporterProvider;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.LoggerFactory;
+
+/**
+ * A simple service locator that is already setup with all components from this library. To acquire a complete
+ * repository system, clients need to add an artifact descriptor reader, a version resolver, a version range resolver
+ * and optionally some repository connector and transporter factories to access remote repositories. Once the locator is
+ * fully populated, the repository system can be created like this:
+ * 
+ * <pre>
+ * RepositorySystem repoSystem = serviceLocator.getService( RepositorySystem.class );
+ * </pre>
+ * 
+ * <em>Note:</em> This class is not thread-safe. Clients are expected to create the service locator and the repository
+ * system on a single thread.
+ */
+public final class DefaultServiceLocator
+    implements ServiceLocator
+{
+
+    private class Entry<T>
+    {
+
+        private final Class<T> type;
+
+        private final Collection<Object> providers;
+
+        private List<T> instances;
+
+        public Entry( Class<T> type )
+        {
+            this.type = requireNonNull( type, "service type cannot be null" );
+            providers = new LinkedHashSet<Object>( 8 );
+        }
+
+        public synchronized void setServices( T... services )
+        {
+            providers.clear();
+            if ( services != null )
+            {
+                for ( T service : services )
+                {
+                    providers.add( requireNonNull( service, "service instance cannot be null" ) );
+                }
+            }
+            instances = null;
+        }
+
+        public synchronized void setService( Class<? extends T> impl )
+        {
+            providers.clear();
+            addService( impl );
+        }
+
+        public synchronized void addService( Class<? extends T> impl )
+        {
+            providers.add( requireNonNull( impl, "implementation class cannot be null" ) );
+            instances = null;
+        }
+
+        public T getInstance()
+        {
+            List<T> instances = getInstances();
+            return instances.isEmpty() ? null : instances.get( 0 );
+        }
+
+        public synchronized List<T> getInstances()
+        {
+            if ( instances == null )
+            {
+                instances = new ArrayList<T>( providers.size() );
+                for ( Object provider : providers )
+                {
+                    T instance;
+                    if ( provider instanceof Class )
+                    {
+                        instance = newInstance( (Class<?>) provider );
+                    }
+                    else
+                    {
+                        instance = type.cast( provider );
+                    }
+                    if ( instance != null )
+                    {
+                        instances.add( instance );
+                    }
+                }
+                instances = Collections.unmodifiableList( instances );
+            }
+            return instances;
+        }
+
+        private T newInstance( Class<?> impl )
+        {
+            try
+            {
+                Constructor<?> constr = impl.getDeclaredConstructor();
+                if ( !Modifier.isPublic( constr.getModifiers() ) )
+                {
+                    constr.setAccessible( true );
+                }
+                Object obj = constr.newInstance();
+
+                T instance = type.cast( obj );
+                if ( instance instanceof Service )
+                {
+                    ( (Service) instance ).initService( DefaultServiceLocator.this );
+                }
+                return instance;
+            }
+            catch ( Exception e )
+            {
+                serviceCreationFailed( type, impl, e );
+            }
+            catch ( LinkageError e )
+            {
+                serviceCreationFailed( type, impl, e );
+            }
+            return null;
+        }
+
+    }
+
+    private final Map<Class<?>, Entry<?>> entries;
+
+    private ErrorHandler errorHandler;
+
+    /**
+     * Creates a new service locator that already knows about all service implementations included this library.
+     */
+    public DefaultServiceLocator()
+    {
+        entries = new HashMap<Class<?>, Entry<?>>();
+
+        addService( RepositorySystem.class, DefaultRepositorySystem.class );
+        addService( ArtifactResolver.class, DefaultArtifactResolver.class );
+        addService( DependencyCollector.class, DefaultDependencyCollector.class );
+        addService( Deployer.class, DefaultDeployer.class );
+        addService( Installer.class, DefaultInstaller.class );
+        addService( MetadataResolver.class, DefaultMetadataResolver.class );
+        addService( RepositoryLayoutProvider.class, DefaultRepositoryLayoutProvider.class );
+        addService( RepositoryLayoutFactory.class, Maven2RepositoryLayoutFactory.class );
+        addService( TransporterProvider.class, DefaultTransporterProvider.class );
+        addService( ChecksumPolicyProvider.class, DefaultChecksumPolicyProvider.class );
+        addService( RepositoryConnectorProvider.class, DefaultRepositoryConnectorProvider.class );
+        addService( RemoteRepositoryManager.class, DefaultRemoteRepositoryManager.class );
+        addService( UpdateCheckManager.class, DefaultUpdateCheckManager.class );
+        addService( UpdatePolicyAnalyzer.class, DefaultUpdatePolicyAnalyzer.class );
+        addService( FileProcessor.class, DefaultFileProcessor.class );
+        addService( SyncContextFactory.class, DefaultSyncContextFactory.class );
+        addService( RepositoryEventDispatcher.class, DefaultRepositoryEventDispatcher.class );
+        addService( OfflineController.class, DefaultOfflineController.class );
+        addService( LocalRepositoryProvider.class, DefaultLocalRepositoryProvider.class );
+        addService( LocalRepositoryManagerFactory.class, SimpleLocalRepositoryManagerFactory.class );
+        addService( LocalRepositoryManagerFactory.class, EnhancedLocalRepositoryManagerFactory.class );
+        if ( Slf4jLoggerFactory.isSlf4jAvailable() )
+        {
+            addService( LoggerFactory.class, Slf4jLoggerFactory.class );
+        }
+    }
+
+    private <T> Entry<T> getEntry( Class<T> type, boolean create )
+    {
+        @SuppressWarnings( "unchecked" )
+        Entry<T> entry = (Entry<T>) entries.get( requireNonNull( type, "service type cannot be null" ) );
+        if ( entry == null && create )
+        {
+            entry = new Entry<T>( type );
+            entries.put( type, entry );
+        }
+        return entry;
+    }
+
+    /**
+     * Sets the implementation class for a service. The specified class must have a no-arg constructor (of any
+     * visibility). If the service implementation itself requires other services for its operation, it should implement
+     * {@link Service} to gain access to this service locator.
+     * 
+     * @param <T> The service type.
+     * @param type The interface describing the service, must not be {@code null}.
+     * @param impl The implementation class of the service, must not be {@code null}.
+     * @return This locator for chaining, never {@code null}.
+     */
+    public <T> DefaultServiceLocator setService( Class<T> type, Class<? extends T> impl )
+    {
+        getEntry( type, true ).setService( impl );
+        return this;
+    }
+
+    /**
+     * Adds an implementation class for a service. The specified class must have a no-arg constructor (of any
+     * visibility). If the service implementation itself requires other services for its operation, it should implement
+     * {@link Service} to gain access to this service locator.
+     * 
+     * @param <T> The service type.
+     * @param type The interface describing the service, must not be {@code null}.
+     * @param impl The implementation class of the service, must not be {@code null}.
+     * @return This locator for chaining, never {@code null}.
+     */
+    public <T> DefaultServiceLocator addService( Class<T> type, Class<? extends T> impl )
+    {
+        getEntry( type, true ).addService( impl );
+        return this;
+    }
+
+    /**
+     * Sets the instances for a service.
+     * 
+     * @param <T> The service type.
+     * @param type The interface describing the service, must not be {@code null}.
+     * @param services The instances of the service, may be {@code null} but must not contain {@code null} elements.
+     * @return This locator for chaining, never {@code null}.
+     */
+    public <T> DefaultServiceLocator setServices( Class<T> type, T... services )
+    {
+        getEntry( type, true ).setServices( services );
+        return this;
+    }
+
+    public <T> T getService( Class<T> type )
+    {
+        Entry<T> entry = getEntry( type, false );
+        return ( entry != null ) ? entry.getInstance() : null;
+    }
+
+    public <T> List<T> getServices( Class<T> type )
+    {
+        Entry<T> entry = getEntry( type, false );
+        return ( entry != null ) ? entry.getInstances() : null;
+    }
+
+    private void serviceCreationFailed( Class<?> type, Class<?> impl, Throwable exception )
+    {
+        if ( errorHandler != null )
+        {
+            errorHandler.serviceCreationFailed( type, impl, exception );
+        }
+    }
+
+    /**
+     * Sets the error handler to use.
+     * 
+     * @param errorHandler The error handler to use, may be {@code null} to ignore/swallow errors.
+     */
+    public void setErrorHandler( ErrorHandler errorHandler )
+    {
+        this.errorHandler = errorHandler;
+    }
+
+    /**
+     * A hook to customize the handling of errors encountered while locating a service implementation.
+     */
+    public abstract static class ErrorHandler
+    {
+
+        /**
+         * Handles errors during creation of a service. The default implemention does nothing.
+         * 
+         * @param type The interface describing the service, must not be {@code null}.
+         * @param impl The implementation class of the service, must not be {@code null}.
+         * @param exception The error that occurred while trying to instantiate the implementation class, must not be
+         *            {@code null}.
+         */
+        public void serviceCreationFailed( Class<?> type, Class<?> impl, Throwable exception )
+        {
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyCollector.java
new file mode 100644
index 0000000..9fa5817
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyCollector.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+
+/**
+ * Given a collection of direct dependencies, recursively gathers their transitive dependencies and calculates the
+ * dependency graph.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface DependencyCollector
+{
+
+    /**
+     * Collects the transitive dependencies of some artifacts and builds a dependency graph. Note that this operation is
+     * only concerned about determining the coordinates of the transitive dependencies and does not actually resolve the
+     * artifact files. The supplied session carries various hooks to customize the dependency graph that must be invoked
+     * throughout the operation.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The collection request, must not be {@code null}.
+     * @return The collection result, never {@code null}.
+     * @throws DependencyCollectionException If the dependency tree could not be built.
+     * @see RepositorySystemSession#getDependencyTraverser()
+     * @see RepositorySystemSession#getDependencyManager()
+     * @see RepositorySystemSession#getDependencySelector()
+     * @see RepositorySystemSession#getVersionFilter()
+     * @see RepositorySystemSession#getDependencyGraphTransformer()
+     * @see RepositorySystem#collectDependencies(RepositorySystemSession, CollectRequest)
+     */
+    CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
+        throws DependencyCollectionException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Deployer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Deployer.java
new file mode 100644
index 0000000..8f6b8fc
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Deployer.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+
+/**
+ * Publishes artifacts to a remote repository.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface Deployer
+{
+
+    /**
+     * Uploads a collection of artifacts and their accompanying metadata to a remote repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The deployment request, must not be {@code null}.
+     * @return The deployment result, never {@code null}.
+     * @throws DeploymentException If any artifact/metadata from the request could not be deployed.
+     * @see RepositorySystem#deploy(RepositorySystemSession, DeployRequest)
+     * @see MetadataGeneratorFactory#newInstance(RepositorySystemSession, DeployRequest)
+     */
+    DeployResult deploy( RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Installer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Installer.java
new file mode 100644
index 0000000..a9ebed6
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/Installer.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+
+/**
+ * Publishes artifacts to the local repository.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface Installer
+{
+
+    /**
+     * Installs a collection of artifacts and their accompanying metadata to the local repository.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The installation request, must not be {@code null}.
+     * @return The installation result, never {@code null}.
+     * @throws InstallationException If any artifact/metadata from the request could not be installed.
+     * @see RepositorySystem#install(RepositorySystemSession, InstallRequest)
+     * @see MetadataGeneratorFactory#newInstance(RepositorySystemSession, InstallRequest)
+     */
+    InstallResult install( RepositorySystemSession session, InstallRequest request )
+        throws InstallationException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/LocalRepositoryProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/LocalRepositoryProvider.java
new file mode 100644
index 0000000..d5f4be2
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/LocalRepositoryProvider.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+
+/**
+ * Retrieves a local repository manager from the installed local repository manager factories.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface LocalRepositoryProvider
+{
+
+    /**
+     * Creates a new manager for the specified local repository. If the specified local repository has no type, the
+     * default local repository type of the system will be used. <em>Note:</em> It is expected that this method
+     * invocation is one of the last steps of setting up a new session, in particular any configuration properties
+     * should have been set already.
+     * 
+     * @param session The repository system session from which to configure the manager, must not be {@code null}.
+     * @param localRepository The local repository to create a manager for, must not be {@code null}.
+     * @return The local repository manager, never {@code null}.
+     * @throws NoLocalRepositoryManagerException If the specified repository type is not recognized or no base directory
+     *             is given.
+     * @see RepositorySystem#newLocalRepositoryManager(RepositorySystemSession, LocalRepository)
+     */
+    LocalRepositoryManager newLocalRepositoryManager( RepositorySystemSession session, LocalRepository localRepository )
+        throws NoLocalRepositoryManagerException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGenerator.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGenerator.java
new file mode 100644
index 0000000..b4356cc
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGenerator.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A metadata generator that participates in the installation/deployment of artifacts.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface MetadataGenerator
+{
+
+    /**
+     * Prepares the generator to transform artifacts.
+     * 
+     * @param artifacts The artifacts to install/deploy, must not be {@code null}.
+     * @return The metadata to process (e.g. merge with existing metadata) before artifact transformations, never
+     *         {@code null}.
+     */
+    Collection<? extends Metadata> prepare( Collection<? extends Artifact> artifacts );
+
+    /**
+     * Enables the metadata generator to transform the specified artifact.
+     * 
+     * @param artifact The artifact to transform, must not be {@code null}.
+     * @return The transformed artifact (or just the input artifact), never {@code null}.
+     */
+    Artifact transformArtifact( Artifact artifact );
+
+    /**
+     * Allows for metadata generation based on the transformed artifacts.
+     * 
+     * @param artifacts The (transformed) artifacts to install/deploy, must not be {@code null}.
+     * @return The additional metadata to process after artifact transformations, never {@code null}.
+     */
+    Collection<? extends Metadata> finish( Collection<? extends Artifact> artifacts );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGeneratorFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGeneratorFactory.java
new file mode 100644
index 0000000..5f2b740
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataGeneratorFactory.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.installation.InstallRequest;
+
+/**
+ * A factory to create metadata generators. Metadata generators can contribute additional metadata during the
+ * installation/deployment of artifacts.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface MetadataGeneratorFactory
+{
+
+    /**
+     * Creates a new metadata generator for the specified install request.
+     * 
+     * @param session The repository system session from which to configure the generator, must not be {@code null}.
+     * @param request The install request the metadata generator is used for, must not be {@code null}.
+     * @return The metadata generator for the request or {@code null} if none.
+     */
+    MetadataGenerator newInstance( RepositorySystemSession session, InstallRequest request );
+
+    /**
+     * Creates a new metadata generator for the specified deploy request.
+     * 
+     * @param session The repository system session from which to configure the generator, must not be {@code null}.
+     * @param request The deploy request the metadata generator is used for, must not be {@code null}.
+     * @return The metadata generator for the request or {@code null} if none.
+     */
+    MetadataGenerator newInstance( RepositorySystemSession session, DeployRequest request );
+
+    /**
+     * The priority of this factory. Factories with higher priority are invoked before those with lower priority.
+     * 
+     * @return The priority of this factory.
+     */
+    float getPriority();
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataResolver.java
new file mode 100644
index 0000000..886e856
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/MetadataResolver.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+
+/**
+ * Resolves metadata, that is gets a local filesystem path to their binary contents.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface MetadataResolver
+{
+
+    /**
+     * Resolves the paths for a collection of metadata. Metadata will be downloaded to the local repository if
+     * necessary, e.g. because it hasn't been cached yet or the cache is deemed outdated.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param requests The resolution requests, must not be {@code null}.
+     * @return The resolution results (in request order), never {@code null}.
+     * @see Metadata#getFile()
+     * @see RepositorySystem#resolveMetadata(RepositorySystemSession, Collection)
+     */
+    List<MetadataResult> resolveMetadata( RepositorySystemSession session,
+                                          Collection<? extends MetadataRequest> requests );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/OfflineController.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/OfflineController.java
new file mode 100644
index 0000000..22f5a4b
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/OfflineController.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+
+/**
+ * Determines whether a remote repository is accessible in offline mode.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface OfflineController
+{
+
+    /**
+     * Determines whether the specified repository is accessible if the system was in offline mode. A simple
+     * implementation might unconditionally throw {@link RepositoryOfflineException} to block all remote repository
+     * access when in offline mode. More sophisticated implementations might inspect
+     * {@link RepositorySystemSession#getConfigProperties() configuration properties} of the session to check for some
+     * kind of whitelist that allows certain remote repositories even when offline. At any rate, the session's current
+     * {@link RepositorySystemSession#isOffline() offline state} is irrelevant to the outcome of the check.
+     * 
+     * @param session The repository session during which the check is made, must not be {@code null}.
+     * @param repository The remote repository to check for offline access, must not be {@code null}.
+     * @throws RepositoryOfflineException If the repository is not accessible in offline mode. If the method returns
+     *             normally, the repository is considered accessible even in offline mode.
+     * @see RepositorySystemSession#isOffline()
+     */
+    void checkOffline( RepositorySystemSession session, RemoteRepository repository )
+        throws RepositoryOfflineException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RemoteRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RemoteRepositoryManager.java
new file mode 100644
index 0000000..23685e7
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RemoteRepositoryManager.java
@@ -0,0 +1,72 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+
+/**
+ * Helps dealing with remote repository definitions.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface RemoteRepositoryManager
+{
+
+    /**
+     * Aggregates repository definitions by merging duplicate repositories and optionally applies mirror, proxy and
+     * authentication settings from the supplied session.
+     * 
+     * @param session The repository session during which the repositories will be accessed, must not be {@code null}.
+     * @param dominantRepositories The current list of remote repositories to merge the new definitions into, must not
+     *            be {@code null}.
+     * @param recessiveRepositories The remote repositories to merge into the existing list, must not be {@code null}.
+     * @param recessiveIsRaw {@code true} if the recessive repository definitions have not yet been subjected to mirror,
+     *            proxy and authentication settings, {@code false} otherwise.
+     * @return The aggregated list of remote repositories, never {@code null}.
+     * @see RepositorySystemSession#getMirrorSelector()
+     * @see RepositorySystemSession#getProxySelector()
+     * @see RepositorySystemSession#getAuthenticationSelector()
+     */
+    List<RemoteRepository> aggregateRepositories( RepositorySystemSession session,
+                                                  List<RemoteRepository> dominantRepositories,
+                                                  List<RemoteRepository> recessiveRepositories, boolean recessiveIsRaw );
+
+    /**
+     * Gets the effective repository policy for the specified remote repository by merging the applicable
+     * snapshot/release policy of the repository with global settings from the supplied session.
+     * 
+     * @param session The repository session during which the repository will be accessed, must not be {@code null}.
+     * @param repository The remote repository to determine the effective policy for, must not be {@code null}.
+     * @param releases {@code true} if the policy for release artifacts needs to be considered, {@code false} if not.
+     * @param snapshots {@code true} if the policy for snapshot artifacts needs to be considered, {@code false} if not.
+     * @return The effective repository policy, never {@code null}.
+     * @see RepositorySystemSession#getChecksumPolicy()
+     * @see RepositorySystemSession#getUpdatePolicy()
+     */
+    RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository, boolean releases,
+                                boolean snapshots );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryConnectorProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryConnectorProvider.java
new file mode 100644
index 0000000..8d665c0
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryConnectorProvider.java
@@ -0,0 +1,49 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+
+/**
+ * Retrieves a repository connector from the installed repository connector factories.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface RepositoryConnectorProvider
+{
+
+    /**
+     * Tries to create a repository connector for the specified remote repository.
+     * 
+     * @param session The repository system session from which to configure the connector, must not be {@code null}.
+     * @param repository The remote repository to create a connector for, must not be {@code null}.
+     * @return The connector for the given repository, never {@code null}.
+     * @throws NoRepositoryConnectorException If no available factory can create a connector for the specified remote
+     *             repository.
+     */
+    RepositoryConnector newRepositoryConnector( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryConnectorException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryEventDispatcher.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryEventDispatcher.java
new file mode 100644
index 0000000..2d29eb7
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/RepositoryEventDispatcher.java
@@ -0,0 +1,41 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryEvent;
+
+/**
+ * Dispatches repository events to registered listeners.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface RepositoryEventDispatcher
+{
+
+    /**
+     * Dispatches the specified repository event to all registered listeners.
+     * 
+     * @param event The event to dispatch, must not be {@code null}.
+     */
+    void dispatch( RepositoryEvent event );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/SyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/SyncContextFactory.java
new file mode 100644
index 0000000..95086d1
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/SyncContextFactory.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+
+/**
+ * A factory to create synchronization contexts. A synchronization context is used to coordinate concurrent access to
+ * artifacts or metadata.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface SyncContextFactory
+{
+
+    /**
+     * Creates a new synchronization context.
+     * 
+     * @param session The repository session during which the context will be used, must not be {@code null}.
+     * @param shared A flag indicating whether access to the artifacts/metadata associated with the new context can be
+     *            shared among concurrent readers or whether access needs to be exclusive to the calling thread.
+     * @return The synchronization context, never {@code null}.
+     * @see RepositorySystem#newSyncContext(RepositorySystemSession, boolean)
+     */
+    SyncContext newInstance( RepositorySystemSession session, boolean shared );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java
new file mode 100644
index 0000000..b77d2bc
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java
@@ -0,0 +1,285 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A request to check if an update of an artifact/metadata from a remote repository is needed.
+ * 
+ * @param <T>
+ * @param <E>
+ * @see UpdateCheckManager
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public final class UpdateCheck<T, E extends RepositoryException>
+{
+
+    private long localLastUpdated;
+
+    private T item;
+
+    private File file;
+
+    private boolean fileValid = true;
+
+    private String policy;
+
+    private RemoteRepository repository;
+
+    private RemoteRepository authoritativeRepository;
+
+    private boolean required;
+
+    private E exception;
+
+    /**
+     * Creates an uninitialized update check request.
+     */
+    public UpdateCheck()
+    {
+    }
+
+    /**
+     * Gets the last-modified timestamp of the corresponding item produced by a local installation. If non-zero, a
+     * remote update will be surpressed if the local item is up-to-date, even if the remote item has not been cached
+     * locally.
+     * 
+     * @return The last-modified timestamp of the corresponding item produced by a local installation or {@code 0} to
+     *         ignore any local item.
+     */
+    public long getLocalLastUpdated()
+    {
+        return localLastUpdated;
+    }
+
+    /**
+     * Sets the last-modified timestamp of the corresponding item produced by a local installation. If non-zero, a
+     * remote update will be surpressed if the local item is up-to-date, even if the remote item has not been cached
+     * locally.
+     * 
+     * @param localLastUpdated The last-modified timestamp of the corresponding item produced by a local installation or
+     *            {@code 0} to ignore any local item.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setLocalLastUpdated( long localLastUpdated )
+    {
+        this.localLastUpdated = localLastUpdated;
+        return this;
+    }
+
+    /**
+     * Gets the item of the check.
+     * 
+     * @return The item of the check, never {@code null}.
+     */
+    public T getItem()
+    {
+        return item;
+    }
+
+    /**
+     * Sets the item of the check.
+     * 
+     * @param item The item of the check, must not be {@code null}.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setItem( T item )
+    {
+        this.item = item;
+        return this;
+    }
+
+    /**
+     * Returns the local file of the item.
+     * 
+     * @return The local file of the item.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the local file of the item.
+     * 
+     * @param file The file of the item, never {@code null} .
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * Indicates whether the local file given by {@link #getFile()}, if existent, should be considered valid or not. An
+     * invalid file is equivalent to a physically missing file.
+     * 
+     * @return {@code true} if the file should be considered valid if existent, {@code false} if the file should be
+     *         treated as if it was missing.
+     */
+    public boolean isFileValid()
+    {
+        return fileValid;
+    }
+
+    /**
+     * Controls whether the local file given by {@link #getFile()}, if existent, should be considered valid or not. An
+     * invalid file is equivalent to a physically missing file.
+     * 
+     * @param fileValid {@code true} if the file should be considered valid if existent, {@code false} if the file
+     *            should be treated as if it was missing.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setFileValid( boolean fileValid )
+    {
+        this.fileValid = fileValid;
+        return this;
+    }
+
+    /**
+     * Gets the policy to use for the check.
+     * 
+     * @return The policy to use for the check.
+     * @see org.eclipse.aether.repository.RepositoryPolicy
+     */
+    public String getPolicy()
+    {
+        return policy;
+    }
+
+    /**
+     * Sets the policy to use for the check.
+     * 
+     * @param policy The policy to use for the check, may be {@code null}.
+     * @return This object for chaining.
+     * @see org.eclipse.aether.repository.RepositoryPolicy
+     */
+    public UpdateCheck<T, E> setPolicy( String policy )
+    {
+        this.policy = policy;
+        return this;
+    }
+
+    /**
+     * Gets the repository from which a potential update/download will performed.
+     * 
+     * @return The repository to use for the check.
+     */
+    public RemoteRepository getRepository()
+    {
+        return repository;
+    }
+
+    /**
+     * Sets the repository from which a potential update/download will performed.
+     * 
+     * @param repository The repository to use for the check, must not be {@code null}.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setRepository( RemoteRepository repository )
+    {
+        this.repository = repository;
+        return this;
+    }
+
+    /**
+     * Gets the repository which ultimately hosts the metadata to update. This will be different from the repository
+     * given by {@link #getRepository()} in case the latter denotes a repository manager.
+     * 
+     * @return The actual repository hosting the authoritative copy of the metadata to update, never {@code null} for a
+     *         metadata update check.
+     */
+    public RemoteRepository getAuthoritativeRepository()
+    {
+        return authoritativeRepository != null ? authoritativeRepository : repository;
+    }
+
+    /**
+     * Sets the repository which ultimately hosts the metadata to update. This will be different from the repository
+     * given by {@link #getRepository()} in case the latter denotes a repository manager.
+     * 
+     * @param authoritativeRepository The actual repository hosting the authoritative copy of the metadata to update,
+     *            must not be {@code null} for a metadata update check.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setAuthoritativeRepository( RemoteRepository authoritativeRepository )
+    {
+        this.authoritativeRepository = authoritativeRepository;
+        return this;
+    }
+
+    /**
+     * Gets the result of a check, denoting whether the remote repository should be checked for updates.
+     * 
+     * @return The result of a check.
+     */
+    public boolean isRequired()
+    {
+        return required;
+    }
+
+    /**
+     * Sets the result of an update check.
+     * 
+     * @param required The result of an update check. In case of {@code false} and the local file given by
+     *            {@link #getFile()} does actually not exist, {@link #setException(RepositoryException)} should be used
+     *            to provide the previous/cached failure that explains the absence of the file.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setRequired( boolean required )
+    {
+        this.required = required;
+        return this;
+    }
+
+    /**
+     * Gets the exception that occurred during the update check.
+     * 
+     * @return The occurred exception or {@code null} if the update check was successful.
+     */
+    public E getException()
+    {
+        return exception;
+    }
+
+    /**
+     * Sets the exception for this update check.
+     * 
+     * @param exception The exception for this update check, may be {@code null} if the check was successful.
+     * @return This object for chaining.
+     */
+    public UpdateCheck<T, E> setException( E exception )
+    {
+        this.exception = exception;
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getPolicy() + ": " + getFile() + " < " + getRepository();
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheckManager.java
new file mode 100644
index 0000000..cd35df0
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheckManager.java
@@ -0,0 +1,70 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+
+/**
+ * Determines if updates of artifacts and metadata from remote repositories are needed.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface UpdateCheckManager
+{
+
+    /**
+     * Checks whether an artifact has to be updated from a remote repository.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param check The update check request, must not be {@code null}.
+     */
+    void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check );
+
+    /**
+     * Updates the timestamp for the artifact contained in the update check.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param check The update check request, must not be {@code null}.
+     */
+    void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check );
+
+    /**
+     * Checks whether metadata has to be updated from a remote repository.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param check The update check request, must not be {@code null}.
+     */
+    void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check );
+
+    /**
+     * Updates the timestamp for the metadata contained in the update check.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param check The update check request, must not be {@code null}.
+     */
+    void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdatePolicyAnalyzer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdatePolicyAnalyzer.java
new file mode 100644
index 0000000..ce8018a
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdatePolicyAnalyzer.java
@@ -0,0 +1,56 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Evaluates update policies.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface UpdatePolicyAnalyzer
+{
+
+    /**
+     * Returns the policy with the shorter update interval.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param policy1 A policy to compare, may be {@code null}.
+     * @param policy2 A policy to compare, may be {@code null}.
+     * @return The policy with the shorter update interval.
+     */
+    String getEffectiveUpdatePolicy( RepositorySystemSession session, String policy1, String policy2 );
+
+    /**
+     * Determines whether the specified modification timestamp satisfies the freshness constraint expressed by the given
+     * update policy.
+     * 
+     * @param session The repository system session during which the check is made, must not be {@code null}.
+     * @param lastModified The timestamp to check against the update policy.
+     * @param policy The update policy, may be {@code null}.
+     * @return {@code true} if the specified timestamp is older than acceptable by the update policy, {@code false}
+     *         otherwise.
+     */
+    boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy );
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionRangeResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionRangeResolver.java
new file mode 100644
index 0000000..89bf706
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionRangeResolver.java
@@ -0,0 +1,55 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+
+/**
+ * Parses and evaluates version ranges encountered in dependency declarations.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface VersionRangeResolver
+{
+
+    /**
+     * Expands a version range to a list of matching versions, in ascending order. For example, resolves "[3.8,4.0)" to
+     * "3.8", "3.8.1", "3.8.2". The returned list of versions is only dependent on the configured repositories and their
+     * contents, the list is not processed by the {@link RepositorySystemSession#getVersionFilter() session's version
+     * filter}.
+     * <p>
+     * The supplied request may also refer to a single concrete version rather than a version range. In this case
+     * though, the result contains simply the (parsed) input version, regardless of the repositories and their contents.
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version range request, must not be {@code null}.
+     * @return The version range result, never {@code null}.
+     * @throws VersionRangeResolutionException If the requested range could not be parsed. Note that an empty range does
+     *             not raise an exception.
+     * @see RepositorySystem#resolveVersionRange(RepositorySystemSession, VersionRangeRequest)
+     */
+    VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionResolver.java
new file mode 100644
index 0000000..e6a8a10
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/VersionResolver.java
@@ -0,0 +1,49 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+
+/**
+ * Evaluates artifact meta/pseudo versions.
+ * 
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ */
+public interface VersionResolver
+{
+
+    /**
+     * Resolves an artifact's meta version (if any) to a concrete version. For example, resolves "1.0-SNAPSHOT" to
+     * "1.0-20090208.132618-23" or "RELEASE"/"LATEST" to "2.0".
+     * 
+     * @param session The repository session, must not be {@code null}.
+     * @param request The version request, must not be {@code null}
+     * @return The version result, never {@code null}.
+     * @throws VersionResolutionException If the metaversion could not be resolved.
+     * @see RepositorySystem#resolveVersion(RepositorySystemSession, VersionRequest)
+     */
+    VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException;
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
new file mode 100644
index 0000000..a19e423
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -0,0 +1,213 @@
+package org.eclipse.aether.impl.guice;
+
+/*
+ * 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.
+ */
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.impl.ArtifactResolver;
+import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.Deployer;
+import org.eclipse.aether.impl.Installer;
+import org.eclipse.aether.impl.LocalRepositoryProvider;
+import org.eclipse.aether.impl.MetadataResolver;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
+import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
+import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
+import org.eclipse.aether.internal.impl.DefaultDependencyCollector;
+import org.eclipse.aether.internal.impl.DefaultDeployer;
+import org.eclipse.aether.internal.impl.DefaultFileProcessor;
+import org.eclipse.aether.internal.impl.DefaultInstaller;
+import org.eclipse.aether.internal.impl.DefaultLocalRepositoryProvider;
+import org.eclipse.aether.internal.impl.DefaultMetadataResolver;
+import org.eclipse.aether.internal.impl.DefaultOfflineController;
+import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
+import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
+import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
+import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
+import org.eclipse.aether.internal.impl.DefaultSyncContextFactory;
+import org.eclipse.aether.internal.impl.DefaultTransporterProvider;
+import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
+import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer;
+import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManagerFactory;
+import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
+import org.eclipse.aether.internal.impl.slf4j.Slf4jLoggerFactory;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.connector.transport.TransporterProvider;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.slf4j.ILoggerFactory;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.name.Names;
+
+/**
+ * A ready-made <a href="https://github.com/google/guice" target="_blank">Guice</a> module that sets up bindings
+ * for all components from this library. To acquire a complete repository system, clients need to bind an artifact
+ * descriptor reader, a version resolver, a version range resolver, zero or more metadata generator factories, some
+ * repository connector and transporter factories to access remote repositories.
+ * 
+ * @noextend This class must not be extended by clients and will eventually be marked {@code final} without prior
+ *           notice.
+ */
+public class AetherModule
+    extends AbstractModule
+{
+
+    /**
+     * Creates a new instance of this Guice module, typically for invoking
+     * {@link com.google.inject.Binder#install(com.google.inject.Module)}.
+     */
+    public AetherModule()
+    {
+    }
+
+    /**
+     * Configures Guice with bindings for Aether components provided by this library.
+     */
+    @Override
+    protected void configure()
+    {
+        bind( RepositorySystem.class ) //
+        .to( DefaultRepositorySystem.class ).in( Singleton.class );
+        bind( ArtifactResolver.class ) //
+        .to( DefaultArtifactResolver.class ).in( Singleton.class );
+        bind( DependencyCollector.class ) //
+        .to( DefaultDependencyCollector.class ).in( Singleton.class );
+        bind( Deployer.class ) //
+        .to( DefaultDeployer.class ).in( Singleton.class );
+        bind( Installer.class ) //
+        .to( DefaultInstaller.class ).in( Singleton.class );
+        bind( MetadataResolver.class ) //
+        .to( DefaultMetadataResolver.class ).in( Singleton.class );
+        bind( RepositoryLayoutProvider.class ) //
+        .to( DefaultRepositoryLayoutProvider.class ).in( Singleton.class );
+        bind( RepositoryLayoutFactory.class ).annotatedWith( Names.named( "maven2" ) ) //
+        .to( Maven2RepositoryLayoutFactory.class ).in( Singleton.class );
+        bind( TransporterProvider.class ) //
+        .to( DefaultTransporterProvider.class ).in( Singleton.class );
+        bind( ChecksumPolicyProvider.class ) //
+        .to( DefaultChecksumPolicyProvider.class ).in( Singleton.class );
+        bind( RepositoryConnectorProvider.class ) //
+        .to( DefaultRepositoryConnectorProvider.class ).in( Singleton.class );
+        bind( RemoteRepositoryManager.class ) //
+        .to( DefaultRemoteRepositoryManager.class ).in( Singleton.class );
+        bind( UpdateCheckManager.class ) //
+        .to( DefaultUpdateCheckManager.class ).in( Singleton.class );
+        bind( UpdatePolicyAnalyzer.class ) //
+        .to( DefaultUpdatePolicyAnalyzer.class ).in( Singleton.class );
+        bind( FileProcessor.class ) //
+        .to( DefaultFileProcessor.class ).in( Singleton.class );
+        bind( SyncContextFactory.class ) //
+        .to( DefaultSyncContextFactory.class ).in( Singleton.class );
+        bind( RepositoryEventDispatcher.class ) //
+        .to( DefaultRepositoryEventDispatcher.class ).in( Singleton.class );
+        bind( OfflineController.class ) //
+        .to( DefaultOfflineController.class ).in( Singleton.class );
+        bind( LocalRepositoryProvider.class ) //
+        .to( DefaultLocalRepositoryProvider.class ).in( Singleton.class );
+        bind( LocalRepositoryManagerFactory.class ).annotatedWith( Names.named( "simple" ) ) //
+        .to( SimpleLocalRepositoryManagerFactory.class ).in( Singleton.class );
+        bind( LocalRepositoryManagerFactory.class ).annotatedWith( Names.named( "enhanced" ) ) //
+        .to( EnhancedLocalRepositoryManagerFactory.class ).in( Singleton.class );
+        if ( Slf4jLoggerFactory.isSlf4jAvailable() )
+        {
+            bindSlf4j();
+        }
+        else
+        {
+            bind( LoggerFactory.class ) //
+            .toInstance( NullLoggerFactory.INSTANCE );
+        }
+
+    }
+
+    private void bindSlf4j()
+    {
+        install( new Slf4jModule() );
+    }
+
+    @Provides
+    @Singleton
+    Set<LocalRepositoryManagerFactory> provideLocalRepositoryManagerFactories( @Named( "simple" ) LocalRepositoryManagerFactory simple,
+                                                                               @Named( "enhanced" ) LocalRepositoryManagerFactory enhanced )
+    {
+        Set<LocalRepositoryManagerFactory> factories = new HashSet<LocalRepositoryManagerFactory>();
+        factories.add( simple );
+        factories.add( enhanced );
+        return Collections.unmodifiableSet( factories );
+    }
+
+    @Provides
+    @Singleton
+    Set<RepositoryLayoutFactory> provideRepositoryLayoutFactories( @Named( "maven2" ) RepositoryLayoutFactory maven2 )
+    {
+        Set<RepositoryLayoutFactory> factories = new HashSet<RepositoryLayoutFactory>();
+        factories.add( maven2 );
+        return Collections.unmodifiableSet( factories );
+    }
+
+    @Provides
+    @Singleton
+    Set<RepositoryListener> providesRepositoryListeners()
+    {
+        return Collections.emptySet();
+    }
+
+    private static class Slf4jModule
+        extends AbstractModule
+    {
+
+        @Override
+        protected void configure()
+        {
+            bind( LoggerFactory.class ) //
+            .to( Slf4jLoggerFactory.class );
+        }
+
+        @Provides
+        @Singleton
+        ILoggerFactory getLoggerFactory()
+        {
+            return org.slf4j.LoggerFactory.getILoggerFactory();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/package-info.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/package-info.java
new file mode 100644
index 0000000..735ae5a
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The integration with the dependency injection framework <a href="https://github.com/google/guice" target="_blank">Google Guice</a>. 
+ */
+package org.eclipse.aether.impl.guice;
+
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/package-info.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/package-info.java
new file mode 100644
index 0000000..959a431
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/package-info.java
@@ -0,0 +1,30 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The provisional interfaces defining the various sub components that implement the repository system. Aether Core
+ * provides stock implementations for most of these components but not all. To obtain a complete/runnable repository
+ * system, the application needs to provide implementations of the following component contracts:
+ * {@link org.eclipse.aether.impl.ArtifactDescriptorReader}, {@link org.eclipse.aether.impl.VersionResolver},
+ * {@link org.eclipse.aether.impl.VersionRangeResolver} and potentially
+ * {@link org.eclipse.aether.impl.MetadataGeneratorFactory}. Said components basically define the file format of the
+ * metadata that is used to reason about an artifact's dependencies and available versions.
+ */
+package org.eclipse.aether.impl;
+
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java
new file mode 100644
index 0000000..368e31b
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.TransferResource;
+
+abstract class AbstractChecksumPolicy
+    implements ChecksumPolicy
+
+{
+
+    protected final Logger logger;
+
+    protected final TransferResource resource;
+
+    protected AbstractChecksumPolicy( LoggerFactory loggerFactory, TransferResource resource )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        this.resource = resource;
+    }
+
+    public boolean onChecksumMatch( String algorithm, int kind )
+    {
+        return true;
+    }
+
+    public void onChecksumMismatch( String algorithm, int kind, ChecksumFailureException exception )
+        throws ChecksumFailureException
+    {
+        if ( ( kind & KIND_UNOFFICIAL ) == 0 )
+        {
+            throw exception;
+        }
+    }
+
+    public void onChecksumError( String algorithm, int kind, ChecksumFailureException exception )
+        throws ChecksumFailureException
+    {
+        logger.debug( "Could not validate " + algorithm + " checksum for " + resource.getResourceName(), exception );
+    }
+
+    public void onNoMoreChecksums()
+        throws ChecksumFailureException
+    {
+        throw new ChecksumFailureException( "Checksum validation failed, no checksums available" );
+    }
+
+    public void onTransferRetry()
+    {
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ArtifactRequestBuilder.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ArtifactRequestBuilder.java
new file mode 100644
index 0000000..f9773dc
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ArtifactRequestBuilder.java
@@ -0,0 +1,68 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+import org.eclipse.aether.resolution.ArtifactRequest;
+
+/**
+ */
+class ArtifactRequestBuilder
+    implements DependencyVisitor
+{
+
+    private final RequestTrace trace;
+
+    private List<ArtifactRequest> requests;
+
+    public ArtifactRequestBuilder( RequestTrace trace )
+    {
+        this.trace = trace;
+        this.requests = new ArrayList<ArtifactRequest>();
+    }
+
+    public List<ArtifactRequest> getRequests()
+    {
+        return requests;
+    }
+
+    public boolean visitEnter( DependencyNode node )
+    {
+        if ( node.getDependency() != null )
+        {
+            ArtifactRequest request = new ArtifactRequest( node );
+            request.setTrace( trace );
+            requests.add( request );
+        }
+
+        return true;
+    }
+
+    public boolean visitLeave( DependencyNode node )
+    {
+        return true;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CacheUtils.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CacheUtils.java
new file mode 100644
index 0000000..d7e8f01
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CacheUtils.java
@@ -0,0 +1,141 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.repository.WorkspaceRepository;
+
+/**
+ * @deprecated To be deleted without replacement.
+ */
+@Deprecated
+public final class CacheUtils
+{
+
+    public static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    public static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+    public static int repositoriesHashCode( List<RemoteRepository> repositories )
+    {
+        int result = 17;
+        for ( RemoteRepository repository : repositories )
+        {
+            result = 31 * result + repositoryHashCode( repository );
+        }
+        return result;
+    }
+
+    private static int repositoryHashCode( RemoteRepository repository )
+    {
+        int result = 17;
+        result = 31 * result + hash( repository.getUrl() );
+        return result;
+    }
+
+    private static boolean repositoryEquals( RemoteRepository r1, RemoteRepository r2 )
+    {
+        if ( r1 == r2 )
+        {
+            return true;
+        }
+
+        return eq( r1.getId(), r2.getId() ) && eq( r1.getUrl(), r2.getUrl() )
+            && policyEquals( r1.getPolicy( false ), r2.getPolicy( false ) )
+            && policyEquals( r1.getPolicy( true ), r2.getPolicy( true ) );
+    }
+
+    private static boolean policyEquals( RepositoryPolicy p1, RepositoryPolicy p2 )
+    {
+        if ( p1 == p2 )
+        {
+            return true;
+        }
+        // update policy doesn't affect contents
+        return p1.isEnabled() == p2.isEnabled() && eq( p1.getChecksumPolicy(), p2.getChecksumPolicy() );
+    }
+
+    public static boolean repositoriesEquals( List<RemoteRepository> r1, List<RemoteRepository> r2 )
+    {
+        if ( r1.size() != r2.size() )
+        {
+            return false;
+        }
+
+        for ( Iterator<RemoteRepository> it1 = r1.iterator(), it2 = r2.iterator(); it1.hasNext(); )
+        {
+            if ( !repositoryEquals( it1.next(), it2.next() ) )
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public static WorkspaceRepository getWorkspace( RepositorySystemSession session )
+    {
+        WorkspaceReader reader = session.getWorkspaceReader();
+        return ( reader != null ) ? reader.getRepository() : null;
+    }
+
+    public static ArtifactRepository getRepository( RepositorySystemSession session,
+                                                    List<RemoteRepository> repositories, Class<?> repoClass,
+                                                    String repoId )
+    {
+        if ( repoClass != null )
+        {
+            if ( WorkspaceRepository.class.isAssignableFrom( repoClass ) )
+            {
+                return session.getWorkspaceReader().getRepository();
+            }
+            else if ( LocalRepository.class.isAssignableFrom( repoClass ) )
+            {
+                return session.getLocalRepository();
+            }
+            else
+            {
+                for ( RemoteRepository repository : repositories )
+                {
+                    if ( repoId.equals( repository.getId() ) )
+                    {
+                        return repository;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CachingArtifactTypeRegistry.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CachingArtifactTypeRegistry.java
new file mode 100644
index 0000000..bde4103
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/CachingArtifactTypeRegistry.java
@@ -0,0 +1,69 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+
+/**
+ * A short-lived artifact type registry that caches results from a presumedly slower type registry.
+ */
+class CachingArtifactTypeRegistry
+    implements ArtifactTypeRegistry
+{
+
+    private final ArtifactTypeRegistry delegate;
+
+    private final Map<String, ArtifactType> types;
+
+    public static ArtifactTypeRegistry newInstance( RepositorySystemSession session )
+    {
+        return newInstance( session.getArtifactTypeRegistry() );
+    }
+
+    public static ArtifactTypeRegistry newInstance( ArtifactTypeRegistry delegate )
+    {
+        return ( delegate != null ) ? new CachingArtifactTypeRegistry( delegate ) : null;
+    }
+
+    private CachingArtifactTypeRegistry( ArtifactTypeRegistry delegate )
+    {
+        this.delegate = delegate;
+        types = new HashMap<String, ArtifactType>();
+    }
+
+    public ArtifactType get( String typeId )
+    {
+        ArtifactType type = types.get( typeId );
+
+        if ( type == null )
+        {
+            type = delegate.get( typeId );
+            types.put( typeId, type );
+        }
+
+        return type;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DataPool.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DataPool.java
new file mode 100644
index 0000000..fdf9ce5
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DataPool.java
@@ -0,0 +1,439 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.eclipse.aether.RepositoryCache;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ */
+final class DataPool
+{
+
+    private static final String ARTIFACT_POOL = DataPool.class.getName() + "$Artifact";
+
+    private static final String DEPENDENCY_POOL = DataPool.class.getName() + "$Dependency";
+
+    private static final String DESCRIPTORS = DataPool.class.getName() + "$Descriptors";
+
+    public static final ArtifactDescriptorResult NO_DESCRIPTOR =
+        new ArtifactDescriptorResult( new ArtifactDescriptorRequest() );
+
+    private ObjectPool<Artifact> artifacts;
+
+    private ObjectPool<Dependency> dependencies;
+
+    private Map<Object, Descriptor> descriptors;
+
+    private Map<Object, Constraint> constraints = new HashMap<Object, Constraint>();
+
+    private Map<Object, List<DependencyNode>> nodes = new HashMap<Object, List<DependencyNode>>( 256 );
+
+    @SuppressWarnings( "unchecked" )
+    public DataPool( RepositorySystemSession session )
+    {
+        RepositoryCache cache = session.getCache();
+
+        if ( cache != null )
+        {
+            artifacts = (ObjectPool<Artifact>) cache.get( session, ARTIFACT_POOL );
+            dependencies = (ObjectPool<Dependency>) cache.get( session, DEPENDENCY_POOL );
+            descriptors = (Map<Object, Descriptor>) cache.get( session, DESCRIPTORS );
+        }
+
+        if ( artifacts == null )
+        {
+            artifacts = new ObjectPool<Artifact>();
+            if ( cache != null )
+            {
+                cache.put( session, ARTIFACT_POOL, artifacts );
+            }
+        }
+
+        if ( dependencies == null )
+        {
+            dependencies = new ObjectPool<Dependency>();
+            if ( cache != null )
+            {
+                cache.put( session, DEPENDENCY_POOL, dependencies );
+            }
+        }
+
+        if ( descriptors == null )
+        {
+            descriptors = Collections.synchronizedMap( new WeakHashMap<Object, Descriptor>( 256 ) );
+            if ( cache != null )
+            {
+                cache.put( session, DESCRIPTORS, descriptors );
+            }
+        }
+    }
+
+    public Artifact intern( Artifact artifact )
+    {
+        return artifacts.intern( artifact );
+    }
+
+    public Dependency intern( Dependency dependency )
+    {
+        return dependencies.intern( dependency );
+    }
+
+    public Object toKey( ArtifactDescriptorRequest request )
+    {
+        return request.getArtifact();
+    }
+
+    public ArtifactDescriptorResult getDescriptor( Object key, ArtifactDescriptorRequest request )
+    {
+        Descriptor descriptor = descriptors.get( key );
+        if ( descriptor != null )
+        {
+            return descriptor.toResult( request );
+        }
+        return null;
+    }
+
+    public void putDescriptor( Object key, ArtifactDescriptorResult result )
+    {
+        descriptors.put( key, new GoodDescriptor( result ) );
+    }
+
+    public void putDescriptor( Object key, ArtifactDescriptorException e )
+    {
+        descriptors.put( key, BadDescriptor.INSTANCE );
+    }
+
+    public Object toKey( VersionRangeRequest request )
+    {
+        return new ConstraintKey( request );
+    }
+
+    public VersionRangeResult getConstraint( Object key, VersionRangeRequest request )
+    {
+        Constraint constraint = constraints.get( key );
+        if ( constraint != null )
+        {
+            return constraint.toResult( request );
+        }
+        return null;
+    }
+
+    public void putConstraint( Object key, VersionRangeResult result )
+    {
+        constraints.put( key, new Constraint( result ) );
+    }
+
+    public Object toKey( Artifact artifact, List<RemoteRepository> repositories, DependencySelector selector,
+                         DependencyManager manager, DependencyTraverser traverser, VersionFilter filter )
+    {
+        return new GraphKey( artifact, repositories, selector, manager, traverser, filter );
+    }
+
+    public List<DependencyNode> getChildren( Object key )
+    {
+        return nodes.get( key );
+    }
+
+    public void putChildren( Object key, List<DependencyNode> children )
+    {
+        nodes.put( key, children );
+    }
+
+    abstract static class Descriptor
+    {
+
+        public abstract ArtifactDescriptorResult toResult( ArtifactDescriptorRequest request );
+
+    }
+
+    static final class GoodDescriptor
+        extends Descriptor
+    {
+
+        final Artifact artifact;
+
+        final List<Artifact> relocations;
+
+        final Collection<Artifact> aliases;
+
+        final List<RemoteRepository> repositories;
+
+        final List<Dependency> dependencies;
+
+        final List<Dependency> managedDependencies;
+
+        public GoodDescriptor( ArtifactDescriptorResult result )
+        {
+            artifact = result.getArtifact();
+            relocations = result.getRelocations();
+            aliases = result.getAliases();
+            dependencies = result.getDependencies();
+            managedDependencies = result.getManagedDependencies();
+            repositories = result.getRepositories();
+        }
+
+        public ArtifactDescriptorResult toResult( ArtifactDescriptorRequest request )
+        {
+            ArtifactDescriptorResult result = new ArtifactDescriptorResult( request );
+            result.setArtifact( artifact );
+            result.setRelocations( relocations );
+            result.setAliases( aliases );
+            result.setDependencies( dependencies );
+            result.setManagedDependencies( managedDependencies );
+            result.setRepositories( repositories );
+            return result;
+        }
+
+    }
+
+    static final class BadDescriptor
+        extends Descriptor
+    {
+
+        static final BadDescriptor INSTANCE = new BadDescriptor();
+
+        public ArtifactDescriptorResult toResult( ArtifactDescriptorRequest request )
+        {
+            return NO_DESCRIPTOR;
+        }
+
+    }
+
+    static final class Constraint
+    {
+
+        final VersionRepo[] repositories;
+
+        final VersionConstraint versionConstraint;
+
+        public Constraint( VersionRangeResult result )
+        {
+            versionConstraint = result.getVersionConstraint();
+            List<Version> versions = result.getVersions();
+            repositories = new VersionRepo[versions.size()];
+            int i = 0;
+            for ( Version version : versions )
+            {
+                repositories[i++] = new VersionRepo( version, result.getRepository( version ) );
+            }
+        }
+
+        public VersionRangeResult toResult( VersionRangeRequest request )
+        {
+            VersionRangeResult result = new VersionRangeResult( request );
+            for ( VersionRepo vr : repositories )
+            {
+                result.addVersion( vr.version );
+                result.setRepository( vr.version, vr.repo );
+            }
+            result.setVersionConstraint( versionConstraint );
+            return result;
+        }
+
+        static final class VersionRepo
+        {
+
+            final Version version;
+
+            final ArtifactRepository repo;
+
+            VersionRepo( Version version, ArtifactRepository repo )
+            {
+                this.version = version;
+                this.repo = repo;
+            }
+
+        }
+
+    }
+
+    static final class ConstraintKey
+    {
+
+        private final Artifact artifact;
+
+        private final List<RemoteRepository> repositories;
+
+        private final int hashCode;
+
+        public ConstraintKey( VersionRangeRequest request )
+        {
+            artifact = request.getArtifact();
+            repositories = request.getRepositories();
+            hashCode = artifact.hashCode();
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof ConstraintKey ) )
+            {
+                return false;
+            }
+            ConstraintKey that = (ConstraintKey) obj;
+            return artifact.equals( that.artifact ) && equals( repositories, that.repositories );
+        }
+
+        private static boolean equals( List<RemoteRepository> repos1, List<RemoteRepository> repos2 )
+        {
+            if ( repos1.size() != repos2.size() )
+            {
+                return false;
+            }
+            for ( int i = 0, n = repos1.size(); i < n; i++ )
+            {
+                RemoteRepository repo1 = repos1.get( i );
+                RemoteRepository repo2 = repos2.get( i );
+                if ( repo1.isRepositoryManager() != repo2.isRepositoryManager() )
+                {
+                    return false;
+                }
+                if ( repo1.isRepositoryManager() )
+                {
+                    if ( !equals( repo1.getMirroredRepositories(), repo2.getMirroredRepositories() ) )
+                    {
+                        return false;
+                    }
+                }
+                else if ( !repo1.getUrl().equals( repo2.getUrl() ) )
+                {
+                    return false;
+                }
+                else if ( repo1.getPolicy( true ).isEnabled() != repo2.getPolicy( true ).isEnabled() )
+                {
+                    return false;
+                }
+                else if ( repo1.getPolicy( false ).isEnabled() != repo2.getPolicy( false ).isEnabled() )
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return hashCode;
+        }
+
+    }
+
+    static final class GraphKey
+    {
+
+        private final Artifact artifact;
+
+        private final List<RemoteRepository> repositories;
+
+        private final DependencySelector selector;
+
+        private final DependencyManager manager;
+
+        private final DependencyTraverser traverser;
+
+        private final VersionFilter filter;
+
+        private final int hashCode;
+
+        public GraphKey( Artifact artifact, List<RemoteRepository> repositories, DependencySelector selector,
+                         DependencyManager manager, DependencyTraverser traverser, VersionFilter filter )
+        {
+            this.artifact = artifact;
+            this.repositories = repositories;
+            this.selector = selector;
+            this.manager = manager;
+            this.traverser = traverser;
+            this.filter = filter;
+
+            int hash = 17;
+            hash = hash * 31 + artifact.hashCode();
+            hash = hash * 31 + repositories.hashCode();
+            hash = hash * 31 + hash( selector );
+            hash = hash * 31 + hash( manager );
+            hash = hash * 31 + hash( traverser );
+            hash = hash * 31 + hash( filter );
+            hashCode = hash;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof GraphKey ) )
+            {
+                return false;
+            }
+            GraphKey that = (GraphKey) obj;
+            return artifact.equals( that.artifact ) && repositories.equals( that.repositories )
+                && eq( selector, that.selector ) && eq( manager, that.manager ) && eq( traverser, that.traverser )
+                && eq( filter, that.filter );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return hashCode;
+        }
+
+        private static <T> boolean eq( T o1, T o2 )
+        {
+            return ( o1 != null ) ? o1.equals( o2 ) : o2 == null;
+        }
+
+        private static int hash( Object o )
+        {
+            return ( o != null ) ? o.hashCode() : 0;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
new file mode 100644
index 0000000..9ccffc8
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
@@ -0,0 +1,742 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.impl.ArtifactResolver;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ */
+@Named
+public class DefaultArtifactResolver
+    implements ArtifactResolver, Service
+{
+
+    private static final String CONFIG_PROP_SNAPSHOT_NORMALIZATION = "aether.artifactResolver.snapshotNormalization";
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private FileProcessor fileProcessor;
+
+    private RepositoryEventDispatcher repositoryEventDispatcher;
+
+    private VersionResolver versionResolver;
+
+    private UpdateCheckManager updateCheckManager;
+
+    private RepositoryConnectorProvider repositoryConnectorProvider;
+
+    private RemoteRepositoryManager remoteRepositoryManager;
+
+    private SyncContextFactory syncContextFactory;
+
+    private OfflineController offlineController;
+
+    public DefaultArtifactResolver()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultArtifactResolver( FileProcessor fileProcessor, RepositoryEventDispatcher repositoryEventDispatcher,
+                             VersionResolver versionResolver, UpdateCheckManager updateCheckManager,
+                             RepositoryConnectorProvider repositoryConnectorProvider,
+                             RemoteRepositoryManager remoteRepositoryManager, SyncContextFactory syncContextFactory,
+                             OfflineController offlineController, LoggerFactory loggerFactory )
+    {
+        setFileProcessor( fileProcessor );
+        setRepositoryEventDispatcher( repositoryEventDispatcher );
+        setVersionResolver( versionResolver );
+        setUpdateCheckManager( updateCheckManager );
+        setRepositoryConnectorProvider( repositoryConnectorProvider );
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setSyncContextFactory( syncContextFactory );
+        setOfflineController( offlineController );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setFileProcessor( locator.getService( FileProcessor.class ) );
+        setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
+        setVersionResolver( locator.getService( VersionResolver.class ) );
+        setUpdateCheckManager( locator.getService( UpdateCheckManager.class ) );
+        setRepositoryConnectorProvider( locator.getService( RepositoryConnectorProvider.class ) );
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
+        setOfflineController( locator.getService( OfflineController.class ) );
+    }
+
+    public DefaultArtifactResolver setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultArtifactResolver setFileProcessor( FileProcessor fileProcessor )
+    {
+        this.fileProcessor = requireNonNull( fileProcessor, "file processor cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
+    {
+        this.repositoryEventDispatcher = requireNonNull( repositoryEventDispatcher, "repository event dispatcher cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setVersionResolver( VersionResolver versionResolver )
+    {
+        this.versionResolver = requireNonNull( versionResolver, "version resolver cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setUpdateCheckManager( UpdateCheckManager updateCheckManager )
+    {
+        this.updateCheckManager = requireNonNull( updateCheckManager, "update check manager cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setRepositoryConnectorProvider( RepositoryConnectorProvider repositoryConnectorProvider )
+    {
+        this.repositoryConnectorProvider = requireNonNull( repositoryConnectorProvider, "repository connector provider cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager = requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setSyncContextFactory( SyncContextFactory syncContextFactory )
+    {
+        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
+        return this;
+    }
+
+    public DefaultArtifactResolver setOfflineController( OfflineController offlineController )
+    {
+        this.offlineController = requireNonNull( offlineController, "offline controller cannot be null" );
+        return this;
+    }
+
+    public ArtifactResult resolveArtifact( RepositorySystemSession session, ArtifactRequest request )
+        throws ArtifactResolutionException
+    {
+        return resolveArtifacts( session, Collections.singleton( request ) ).get( 0 );
+    }
+
+    public List<ArtifactResult> resolveArtifacts( RepositorySystemSession session,
+                                                  Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException
+    {
+        SyncContext syncContext = syncContextFactory.newInstance( session, false );
+
+        try
+        {
+            Collection<Artifact> artifacts = new ArrayList<Artifact>( requests.size() );
+            for ( ArtifactRequest request : requests )
+            {
+                if ( request.getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) != null )
+                {
+                    continue;
+                }
+                artifacts.add( request.getArtifact() );
+            }
+
+            syncContext.acquire( artifacts, null );
+
+            return resolve( session, requests );
+        }
+        finally
+        {
+            syncContext.close();
+        }
+    }
+
+    private List<ArtifactResult> resolve( RepositorySystemSession session,
+                                          Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException
+    {
+        List<ArtifactResult> results = new ArrayList<ArtifactResult>( requests.size() );
+        boolean failures = false;
+
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+        WorkspaceReader workspace = session.getWorkspaceReader();
+
+        List<ResolutionGroup> groups = new ArrayList<ResolutionGroup>();
+
+        for ( ArtifactRequest request : requests )
+        {
+            RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+            ArtifactResult result = new ArtifactResult( request );
+            results.add( result );
+
+            Artifact artifact = request.getArtifact();
+            List<RemoteRepository> repos = request.getRepositories();
+
+            artifactResolving( session, trace, artifact );
+
+            String localPath = artifact.getProperty( ArtifactProperties.LOCAL_PATH, null );
+            if ( localPath != null )
+            {
+                // unhosted artifact, just validate file
+                File file = new File( localPath );
+                if ( !file.isFile() )
+                {
+                    failures = true;
+                    result.addException( new ArtifactNotFoundException( artifact, null ) );
+                }
+                else
+                {
+                    artifact = artifact.setFile( file );
+                    result.setArtifact( artifact );
+                    artifactResolved( session, trace, artifact, null, result.getExceptions() );
+                }
+                continue;
+            }
+
+            VersionResult versionResult;
+            try
+            {
+                VersionRequest versionRequest = new VersionRequest( artifact, repos, request.getRequestContext() );
+                versionRequest.setTrace( trace );
+                versionResult = versionResolver.resolveVersion( session, versionRequest );
+            }
+            catch ( VersionResolutionException e )
+            {
+                result.addException( e );
+                continue;
+            }
+
+            artifact = artifact.setVersion( versionResult.getVersion() );
+
+            if ( versionResult.getRepository() != null )
+            {
+                if ( versionResult.getRepository() instanceof RemoteRepository )
+                {
+                    repos = Collections.singletonList( (RemoteRepository) versionResult.getRepository() );
+                }
+                else
+                {
+                    repos = Collections.emptyList();
+                }
+            }
+
+            if ( workspace != null )
+            {
+                File file = workspace.findArtifact( artifact );
+                if ( file != null )
+                {
+                    artifact = artifact.setFile( file );
+                    result.setArtifact( artifact );
+                    result.setRepository( workspace.getRepository() );
+                    artifactResolved( session, trace, artifact, result.getRepository(), null );
+                    continue;
+                }
+            }
+
+            LocalArtifactResult local =
+                lrm.find( session, new LocalArtifactRequest( artifact, repos, request.getRequestContext() ) );
+            if ( isLocallyInstalled( local, versionResult ) )
+            {
+                if ( local.getRepository() != null )
+                {
+                    result.setRepository( local.getRepository() );
+                }
+                else
+                {
+                    result.setRepository( lrm.getRepository() );
+                }
+                try
+                {
+                    artifact = artifact.setFile( getFile( session, artifact, local.getFile() ) );
+                    result.setArtifact( artifact );
+                    artifactResolved( session, trace, artifact, result.getRepository(), null );
+                }
+                catch ( ArtifactTransferException e )
+                {
+                    result.addException( e );
+                }
+                if ( !local.isAvailable() )
+                {
+                    /*
+                     * NOTE: Interop with simple local repository: An artifact installed by a simple local repo manager
+                     * will not show up in the repository tracking file of the enhanced local repository. If however the
+                     * maven-metadata-local.xml tells us the artifact was installed locally, we sync the repository
+                     * tracking file.
+                     */
+                    lrm.add( session, new LocalArtifactRegistration( artifact ) );
+                }
+                continue;
+            }
+            else if ( local.getFile() != null )
+            {
+                logger.debug( "Verifying availability of " + local.getFile() + " from " + repos );
+            }
+
+            AtomicBoolean resolved = new AtomicBoolean( false );
+            Iterator<ResolutionGroup> groupIt = groups.iterator();
+            for ( RemoteRepository repo : repos )
+            {
+                if ( !repo.getPolicy( artifact.isSnapshot() ).isEnabled() )
+                {
+                    continue;
+                }
+
+                try
+                {
+                    Utils.checkOffline( session, offlineController, repo );
+                }
+                catch ( RepositoryOfflineException e )
+                {
+                    Exception exception =
+                        new ArtifactNotFoundException( artifact, repo, "Cannot access " + repo.getId() + " ("
+                            + repo.getUrl() + ") in offline mode and the artifact " + artifact
+                            + " has not been downloaded from it before.", e );
+                    result.addException( exception );
+                    continue;
+                }
+
+                ResolutionGroup group = null;
+                while ( groupIt.hasNext() )
+                {
+                    ResolutionGroup t = groupIt.next();
+                    if ( t.matches( repo ) )
+                    {
+                        group = t;
+                        break;
+                    }
+                }
+                if ( group == null )
+                {
+                    group = new ResolutionGroup( repo );
+                    groups.add( group );
+                    groupIt = Collections.<ResolutionGroup>emptyList().iterator();
+                }
+                group.items.add( new ResolutionItem( trace, artifact, resolved, result, local, repo ) );
+            }
+        }
+
+        for ( ResolutionGroup group : groups )
+        {
+            performDownloads( session, group );
+        }
+
+        for ( ArtifactResult result : results )
+        {
+            ArtifactRequest request = result.getRequest();
+
+            Artifact artifact = result.getArtifact();
+            if ( artifact == null || artifact.getFile() == null )
+            {
+                failures = true;
+                if ( result.getExceptions().isEmpty() )
+                {
+                    Exception exception = new ArtifactNotFoundException( request.getArtifact(), null );
+                    result.addException( exception );
+                }
+                RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+                artifactResolved( session, trace, request.getArtifact(), null, result.getExceptions() );
+            }
+        }
+
+        if ( failures )
+        {
+            throw new ArtifactResolutionException( results );
+        }
+
+        return results;
+    }
+
+    private boolean isLocallyInstalled( LocalArtifactResult lar, VersionResult vr )
+    {
+        if ( lar.isAvailable() )
+        {
+            return true;
+        }
+        if ( lar.getFile() != null )
+        {
+            if ( vr.getRepository() instanceof LocalRepository )
+            {
+                // resolution of (snapshot) version found locally installed artifact
+                return true;
+            }
+            else if ( vr.getRepository() == null && lar.getRequest().getRepositories().isEmpty() )
+            {
+                // resolution of version range found locally installed artifact
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private File getFile( RepositorySystemSession session, Artifact artifact, File file )
+        throws ArtifactTransferException
+    {
+        if ( artifact.isSnapshot() && !artifact.getVersion().equals( artifact.getBaseVersion() )
+            && ConfigUtils.getBoolean( session, true, CONFIG_PROP_SNAPSHOT_NORMALIZATION ) )
+        {
+            String name = file.getName().replace( artifact.getVersion(), artifact.getBaseVersion() );
+            File dst = new File( file.getParent(), name );
+
+            boolean copy = dst.length() != file.length() || dst.lastModified() != file.lastModified();
+            if ( copy )
+            {
+                try
+                {
+                    fileProcessor.copy( file, dst );
+                    dst.setLastModified( file.lastModified() );
+                }
+                catch ( IOException e )
+                {
+                    throw new ArtifactTransferException( artifact, null, e );
+                }
+            }
+
+            file = dst;
+        }
+
+        return file;
+    }
+
+    private void performDownloads( RepositorySystemSession session, ResolutionGroup group )
+    {
+        List<ArtifactDownload> downloads = gatherDownloads( session, group );
+        if ( downloads.isEmpty() )
+        {
+            return;
+        }
+
+        for ( ArtifactDownload download : downloads )
+        {
+            artifactDownloading( session, download.getTrace(), download.getArtifact(), group.repository );
+        }
+
+        try
+        {
+            RepositoryConnector connector =
+                repositoryConnectorProvider.newRepositoryConnector( session, group.repository );
+            try
+            {
+                connector.get( downloads, null );
+            }
+            finally
+            {
+                connector.close();
+            }
+        }
+        catch ( NoRepositoryConnectorException e )
+        {
+            for ( ArtifactDownload download : downloads )
+            {
+                download.setException( new ArtifactTransferException( download.getArtifact(), group.repository, e ) );
+            }
+        }
+
+        evaluateDownloads( session, group );
+    }
+
+    private List<ArtifactDownload> gatherDownloads( RepositorySystemSession session, ResolutionGroup group )
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+        List<ArtifactDownload> downloads = new ArrayList<ArtifactDownload>();
+
+        for ( ResolutionItem item : group.items )
+        {
+            Artifact artifact = item.artifact;
+
+            if ( item.resolved.get() )
+            {
+                // resolved in previous resolution group
+                continue;
+            }
+
+            ArtifactDownload download = new ArtifactDownload();
+            download.setArtifact( artifact );
+            download.setRequestContext( item.request.getRequestContext() );
+            download.setListener( SafeTransferListener.wrap( session, logger ) );
+            download.setTrace( item.trace );
+            if ( item.local.getFile() != null )
+            {
+                download.setFile( item.local.getFile() );
+                download.setExistenceCheck( true );
+            }
+            else
+            {
+                String path =
+                    lrm.getPathForRemoteArtifact( artifact, group.repository, item.request.getRequestContext() );
+                download.setFile( new File( lrm.getRepository().getBasedir(), path ) );
+            }
+
+            boolean snapshot = artifact.isSnapshot();
+            RepositoryPolicy policy =
+                remoteRepositoryManager.getPolicy( session, group.repository, !snapshot, snapshot );
+
+            int errorPolicy = Utils.getPolicy( session, artifact, group.repository );
+            if ( ( errorPolicy & ResolutionErrorPolicy.CACHE_ALL ) != 0 )
+            {
+                UpdateCheck<Artifact, ArtifactTransferException> check =
+                    new UpdateCheck<Artifact, ArtifactTransferException>();
+                check.setItem( artifact );
+                check.setFile( download.getFile() );
+                check.setFileValid( false );
+                check.setRepository( group.repository );
+                check.setPolicy( policy.getUpdatePolicy() );
+                item.updateCheck = check;
+                updateCheckManager.checkArtifact( session, check );
+                if ( !check.isRequired() )
+                {
+                    item.result.addException( check.getException() );
+                    continue;
+                }
+            }
+
+            download.setChecksumPolicy( policy.getChecksumPolicy() );
+            download.setRepositories( item.repository.getMirroredRepositories() );
+            downloads.add( download );
+            item.download = download;
+        }
+
+        return downloads;
+    }
+
+    private void evaluateDownloads( RepositorySystemSession session, ResolutionGroup group )
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+
+        for ( ResolutionItem item : group.items )
+        {
+            ArtifactDownload download = item.download;
+            if ( download == null )
+            {
+                continue;
+            }
+
+            Artifact artifact = download.getArtifact();
+            if ( download.getException() == null )
+            {
+                item.resolved.set( true );
+                item.result.setRepository( group.repository );
+                try
+                {
+                    artifact = artifact.setFile( getFile( session, artifact, download.getFile() ) );
+                    item.result.setArtifact( artifact );
+
+                    lrm.add( session,
+                             new LocalArtifactRegistration( artifact, group.repository, download.getSupportedContexts() ) );
+                }
+                catch ( ArtifactTransferException e )
+                {
+                    download.setException( e );
+                    item.result.addException( e );
+                }
+            }
+            else
+            {
+                item.result.addException( download.getException() );
+            }
+
+            /*
+             * NOTE: Touch after registration with local repo to ensure concurrent resolution is not rejected with
+             * "already updated" via session data when actual update to local repo is still pending.
+             */
+            if ( item.updateCheck != null )
+            {
+                item.updateCheck.setException( download.getException() );
+                updateCheckManager.touchArtifact( session, item.updateCheck );
+            }
+
+            artifactDownloaded( session, download.getTrace(), artifact, group.repository, download.getException() );
+            if ( download.getException() == null )
+            {
+                artifactResolved( session, download.getTrace(), artifact, group.repository, null );
+            }
+        }
+    }
+
+    private void artifactResolving( RepositorySystemSession session, RequestTrace trace, Artifact artifact )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_RESOLVING );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void artifactResolved( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                                   ArtifactRepository repository, List<Exception> exceptions )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_RESOLVED );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+        event.setRepository( repository );
+        event.setExceptions( exceptions );
+        if ( artifact != null )
+        {
+            event.setFile( artifact.getFile() );
+        }
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void artifactDownloading( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                                      RemoteRepository repository )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DOWNLOADING );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+        event.setRepository( repository );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void artifactDownloaded( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                                     RemoteRepository repository, Exception exception )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DOWNLOADED );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+        event.setRepository( repository );
+        event.setException( exception );
+        if ( artifact != null )
+        {
+            event.setFile( artifact.getFile() );
+        }
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    static class ResolutionGroup
+    {
+
+        final RemoteRepository repository;
+
+        final List<ResolutionItem> items = new ArrayList<ResolutionItem>();
+
+        ResolutionGroup( RemoteRepository repository )
+        {
+            this.repository = repository;
+        }
+
+        boolean matches( RemoteRepository repo )
+        {
+            return repository.getUrl().equals( repo.getUrl() )
+                && repository.getContentType().equals( repo.getContentType() )
+                && repository.isRepositoryManager() == repo.isRepositoryManager();
+        }
+
+    }
+
+    static class ResolutionItem
+    {
+
+        final RequestTrace trace;
+
+        final ArtifactRequest request;
+
+        final ArtifactResult result;
+
+        final LocalArtifactResult local;
+
+        final RemoteRepository repository;
+
+        final Artifact artifact;
+
+        final AtomicBoolean resolved;
+
+        ArtifactDownload download;
+
+        UpdateCheck<Artifact, ArtifactTransferException> updateCheck;
+
+        ResolutionItem( RequestTrace trace, Artifact artifact, AtomicBoolean resolved, ArtifactResult result,
+                        LocalArtifactResult local, RemoteRepository repository )
+        {
+            this.trace = trace;
+            this.artifact = artifact;
+            this.resolved = resolved;
+            this.result = result;
+            this.request = result.getRequest();
+            this.local = local;
+            this.repository = repository;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProvider.java
new file mode 100644
index 0000000..20c0484
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProvider.java
@@ -0,0 +1,121 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.TransferResource;
+
+/**
+ */
+@Named
+public final class DefaultChecksumPolicyProvider
+    implements ChecksumPolicyProvider, Service
+{
+
+    private static final int ORDINAL_IGNORE = 0;
+
+    private static final int ORDINAL_WARN = 1;
+
+    private static final int ORDINAL_FAIL = 2;
+
+    private LoggerFactory loggerFactory = NullLoggerFactory.INSTANCE;
+
+    public DefaultChecksumPolicyProvider()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultChecksumPolicyProvider( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    public DefaultChecksumPolicyProvider setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.loggerFactory = loggerFactory;
+        return this;
+    }
+
+    public ChecksumPolicy newChecksumPolicy( RepositorySystemSession session, RemoteRepository repository,
+                                             TransferResource resource, String policy )
+    {
+        if ( RepositoryPolicy.CHECKSUM_POLICY_IGNORE.equals( policy ) )
+        {
+            return null;
+        }
+        if ( RepositoryPolicy.CHECKSUM_POLICY_FAIL.equals( policy ) )
+        {
+            return new FailChecksumPolicy( loggerFactory, resource );
+        }
+        return new WarnChecksumPolicy( loggerFactory, resource );
+    }
+
+    public String getEffectiveChecksumPolicy( RepositorySystemSession session, String policy1, String policy2 )
+    {
+        if ( policy1 != null && policy1.equals( policy2 ) )
+        {
+            return policy1;
+        }
+        int ordinal1 = ordinalOfPolicy( policy1 );
+        int ordinal2 = ordinalOfPolicy( policy2 );
+        if ( ordinal2 < ordinal1 )
+        {
+            return ( ordinal2 != ORDINAL_WARN ) ? policy2 : RepositoryPolicy.CHECKSUM_POLICY_WARN;
+        }
+        else
+        {
+            return ( ordinal1 != ORDINAL_WARN ) ? policy1 : RepositoryPolicy.CHECKSUM_POLICY_WARN;
+        }
+    }
+
+    private static int ordinalOfPolicy( String policy )
+    {
+        if ( RepositoryPolicy.CHECKSUM_POLICY_FAIL.equals( policy ) )
+        {
+            return ORDINAL_FAIL;
+        }
+        else if ( RepositoryPolicy.CHECKSUM_POLICY_IGNORE.equals( policy ) )
+        {
+            return ORDINAL_IGNORE;
+        }
+        else
+        {
+            return ORDINAL_WARN;
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectionContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectionContext.java
new file mode 100644
index 0000000..1ad6cc7
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectionContext.java
@@ -0,0 +1,86 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * @see DefaultDependencyCollector
+ */
+final class DefaultDependencyCollectionContext
+    implements DependencyCollectionContext
+{
+
+    private final RepositorySystemSession session;
+
+    private Artifact artifact;
+
+    private Dependency dependency;
+
+    private List<Dependency> managedDependencies;
+
+    public DefaultDependencyCollectionContext( RepositorySystemSession session, Artifact artifact,
+                                               Dependency dependency, List<Dependency> managedDependencies )
+    {
+        this.session = session;
+        this.artifact = ( dependency != null ) ? dependency.getArtifact() : artifact;
+        this.dependency = dependency;
+        this.managedDependencies = managedDependencies;
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    public void set( Dependency dependency, List<Dependency> managedDependencies )
+    {
+        artifact = dependency.getArtifact();
+        this.dependency = dependency;
+        this.managedDependencies = managedDependencies;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getDependency() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java
new file mode 100644
index 0000000..353f0c4
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCollector.java
@@ -0,0 +1,896 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
+import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
+import org.eclipse.aether.version.Version;
+
+/**
+ */
+@Named
+public class DefaultDependencyCollector
+    implements DependencyCollector, Service
+{
+
+    private static final String CONFIG_PROP_MAX_EXCEPTIONS = "aether.dependencyCollector.maxExceptions";
+
+    private static final String CONFIG_PROP_MAX_CYCLES = "aether.dependencyCollector.maxCycles";
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private RemoteRepositoryManager remoteRepositoryManager;
+
+    private ArtifactDescriptorReader descriptorReader;
+
+    private VersionRangeResolver versionRangeResolver;
+
+    public DefaultDependencyCollector()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
+                                ArtifactDescriptorReader artifactDescriptorReader,
+                                VersionRangeResolver versionRangeResolver, LoggerFactory loggerFactory )
+    {
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setArtifactDescriptorReader( artifactDescriptorReader );
+        setVersionRangeResolver( versionRangeResolver );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
+        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
+    }
+
+    public DefaultDependencyCollector setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager = requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
+        return this;
+    }
+
+    public DefaultDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
+    {
+        descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
+        return this;
+    }
+
+    public DefaultDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
+    {
+        this.versionRangeResolver = requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
+        return this;
+    }
+
+    public CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
+        throws DependencyCollectionException
+    {
+        session = optimizeSession( session );
+
+        RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+        CollectResult result = new CollectResult( request );
+
+        DependencySelector depSelector = session.getDependencySelector();
+        DependencyManager depManager = session.getDependencyManager();
+        DependencyTraverser depTraverser = session.getDependencyTraverser();
+        VersionFilter verFilter = session.getVersionFilter();
+
+        Dependency root = request.getRoot();
+        List<RemoteRepository> repositories = request.getRepositories();
+        List<Dependency> dependencies = request.getDependencies();
+        List<Dependency> managedDependencies = request.getManagedDependencies();
+
+        Map<String, Object> stats = logger.isDebugEnabled() ? new LinkedHashMap<String, Object>() : null;
+        long time1 = System.nanoTime();
+
+        DefaultDependencyNode node;
+        if ( root != null )
+        {
+            List<? extends Version> versions;
+            VersionRangeResult rangeResult;
+            try
+            {
+                VersionRangeRequest rangeRequest =
+                    new VersionRangeRequest( root.getArtifact(), request.getRepositories(),
+                                             request.getRequestContext() );
+                rangeRequest.setTrace( trace );
+                rangeResult = versionRangeResolver.resolveVersionRange( session, rangeRequest );
+                versions = filterVersions( root, rangeResult, verFilter, new DefaultVersionFilterContext( session ) );
+            }
+            catch ( VersionRangeResolutionException e )
+            {
+                result.addException( e );
+                throw new DependencyCollectionException( result, e.getMessage() );
+            }
+
+            Version version = versions.get( versions.size() - 1 );
+            root = root.setArtifact( root.getArtifact().setVersion( version.toString() ) );
+
+            ArtifactDescriptorResult descriptorResult;
+            try
+            {
+                ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
+                descriptorRequest.setArtifact( root.getArtifact() );
+                descriptorRequest.setRepositories( request.getRepositories() );
+                descriptorRequest.setRequestContext( request.getRequestContext() );
+                descriptorRequest.setTrace( trace );
+                if ( isLackingDescriptor( root.getArtifact() ) )
+                {
+                    descriptorResult = new ArtifactDescriptorResult( descriptorRequest );
+                }
+                else
+                {
+                    descriptorResult = descriptorReader.readArtifactDescriptor( session, descriptorRequest );
+                }
+            }
+            catch ( ArtifactDescriptorException e )
+            {
+                result.addException( e );
+                throw new DependencyCollectionException( result, e.getMessage() );
+            }
+
+            root = root.setArtifact( descriptorResult.getArtifact() );
+
+            if ( !session.isIgnoreArtifactDescriptorRepositories() )
+            {
+                repositories = remoteRepositoryManager.aggregateRepositories( session, repositories,
+                                                                              descriptorResult.getRepositories(),
+                                                                              true );
+            }
+            dependencies = mergeDeps( dependencies, descriptorResult.getDependencies() );
+            managedDependencies = mergeDeps( managedDependencies, descriptorResult.getManagedDependencies() );
+
+            node = new DefaultDependencyNode( root );
+            node.setRequestContext( request.getRequestContext() );
+            node.setRelocations( descriptorResult.getRelocations() );
+            node.setVersionConstraint( rangeResult.getVersionConstraint() );
+            node.setVersion( version );
+            node.setAliases( descriptorResult.getAliases() );
+            node.setRepositories( request.getRepositories() );
+        }
+        else
+        {
+            node = new DefaultDependencyNode( request.getRootArtifact() );
+            node.setRequestContext( request.getRequestContext() );
+            node.setRepositories( request.getRepositories() );
+        }
+
+        result.setRoot( node );
+
+        boolean traverse = root == null || depTraverser == null || depTraverser.traverseDependency( root );
+        String errorPath = null;
+        if ( traverse && !dependencies.isEmpty() )
+        {
+            DataPool pool = new DataPool( session );
+
+            NodeStack nodes = new NodeStack();
+            nodes.push( node );
+
+            DefaultDependencyCollectionContext context =
+                new DefaultDependencyCollectionContext( session, request.getRootArtifact(), root, managedDependencies );
+
+            DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
+
+            Args args = new Args( session, trace, pool, nodes, context, versionContext, request );
+            Results results = new Results( result, session );
+
+            process( args, results, dependencies, repositories,
+                     depSelector != null ? depSelector.deriveChildSelector( context ) : null,
+                     depManager != null ? depManager.deriveChildManager( context ) : null,
+                     depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null,
+                     verFilter != null ? verFilter.deriveChildFilter( context ) : null );
+
+            errorPath = results.errorPath;
+        }
+
+        long time2 = System.nanoTime();
+
+        DependencyGraphTransformer transformer = session.getDependencyGraphTransformer();
+        if ( transformer != null )
+        {
+            try
+            {
+                DefaultDependencyGraphTransformationContext context =
+                    new DefaultDependencyGraphTransformationContext( session );
+                context.put( TransformationContextKeys.STATS, stats );
+                result.setRoot( transformer.transformGraph( node, context ) );
+            }
+            catch ( RepositoryException e )
+            {
+                result.addException( e );
+            }
+        }
+
+        if ( stats != null )
+        {
+            long time3 = System.nanoTime();
+            stats.put( "DefaultDependencyCollector.collectTime", time2 - time1 );
+            stats.put( "DefaultDependencyCollector.transformTime", time3 - time2 );
+            logger.debug( "Dependency collection stats: " + stats );
+        }
+
+        if ( errorPath != null )
+        {
+            throw new DependencyCollectionException( result, "Failed to collect dependencies at " + errorPath );
+        }
+        if ( !result.getExceptions().isEmpty() )
+        {
+            throw new DependencyCollectionException( result );
+        }
+
+        return result;
+    }
+
+    private static RepositorySystemSession optimizeSession( RepositorySystemSession session )
+    {
+        DefaultRepositorySystemSession optimized = new DefaultRepositorySystemSession( session );
+        optimized.setArtifactTypeRegistry( CachingArtifactTypeRegistry.newInstance( session ) );
+        return optimized;
+    }
+
+    private List<Dependency> mergeDeps( List<Dependency> dominant, List<Dependency> recessive )
+    {
+        List<Dependency> result;
+        if ( dominant == null || dominant.isEmpty() )
+        {
+            result = recessive;
+        }
+        else if ( recessive == null || recessive.isEmpty() )
+        {
+            result = dominant;
+        }
+        else
+        {
+            int initialCapacity = dominant.size() + recessive.size();
+            result = new ArrayList<Dependency>( initialCapacity );
+            Collection<String> ids = new HashSet<String>( initialCapacity, 1.0f );
+            for ( Dependency dependency : dominant )
+            {
+                ids.add( getId( dependency.getArtifact() ) );
+                result.add( dependency );
+            }
+            for ( Dependency dependency : recessive )
+            {
+                if ( !ids.contains( getId( dependency.getArtifact() ) ) )
+                {
+                    result.add( dependency );
+                }
+            }
+        }
+        return result;
+    }
+
+    private static String getId( Artifact a )
+    {
+        return a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getClassifier() + ':' + a.getExtension();
+    }
+
+    private void process( final Args args, Results results, List<Dependency> dependencies,
+                          List<RemoteRepository> repositories, DependencySelector depSelector,
+                          DependencyManager depManager, DependencyTraverser depTraverser, VersionFilter verFilter )
+    {
+        for ( Dependency dependency : dependencies )
+        {
+            processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter,
+                               dependency );
+        }
+    }
+
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency )
+    {
+
+        List<Artifact> relocations = Collections.emptyList();
+        boolean disableVersionManagement = false;
+        processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, dependency,
+                           relocations, disableVersionManagement );
+    }
+
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency,
+                                    List<Artifact> relocations, boolean disableVersionManagement )
+    {
+
+        if ( depSelector != null && !depSelector.selectDependency( dependency ) )
+        {
+            return;
+        }
+
+        PremanagedDependency preManaged =
+            PremanagedDependency.create( depManager, dependency, disableVersionManagement, args.premanagedState );
+        dependency = preManaged.managedDependency;
+
+        boolean noDescriptor = isLackingDescriptor( dependency.getArtifact() );
+
+        boolean traverse = !noDescriptor && ( depTraverser == null || depTraverser.traverseDependency( dependency ) );
+
+        List<? extends Version> versions;
+        VersionRangeResult rangeResult;
+        try
+        {
+            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, repositories, dependency );
+
+            rangeResult = cachedResolveRangeResult( rangeRequest, args.pool, args.session );
+
+            versions = filterVersions( dependency, rangeResult, verFilter, args.versionContext );
+        }
+        catch ( VersionRangeResolutionException e )
+        {
+            results.addException( dependency, e, args.nodes );
+            return;
+        }
+
+        for ( Version version : versions )
+        {
+            Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
+            Dependency d = dependency.setArtifact( originalArtifact );
+
+            ArtifactDescriptorRequest descriptorRequest = createArtifactDescriptorRequest( args, repositories, d );
+
+            final ArtifactDescriptorResult descriptorResult =
+                getArtifactDescriptorResult( args, results, noDescriptor, d, descriptorRequest );
+            if ( descriptorResult != null )
+            {
+                d = d.setArtifact( descriptorResult.getArtifact() );
+
+                DependencyNode node = args.nodes.top();
+
+                int cycleEntry = args.nodes.find( d.getArtifact() );
+                if ( cycleEntry >= 0 )
+                {
+                    results.addCycle( args.nodes, cycleEntry, d );
+                    DependencyNode cycleNode = args.nodes.get( cycleEntry );
+                    if ( cycleNode.getDependency() != null )
+                    {
+                        DefaultDependencyNode child =
+                            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult,
+                                                  cycleNode );
+                        node.getChildren().add( child );
+                        continue;
+                    }
+                }
+
+                if ( !descriptorResult.getRelocations().isEmpty() )
+                {
+                    boolean disableVersionManagementSubsequently =
+                        originalArtifact.getGroupId().equals( d.getArtifact().getGroupId() )
+                            && originalArtifact.getArtifactId().equals( d.getArtifact().getArtifactId() );
+
+                    processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                       descriptorResult.getRelocations(), disableVersionManagementSubsequently );
+                    return;
+                }
+                else
+                {
+                    d = args.pool.intern( d.setArtifact( args.pool.intern( d.getArtifact() ) ) );
+
+                    List<RemoteRepository> repos =
+                        getRemoteRepositories( rangeResult.getRepository( version ), repositories );
+
+                    DefaultDependencyNode child =
+                        createDependencyNode( relocations, preManaged, rangeResult, version, d,
+                                              descriptorResult.getAliases(), repos, args.request.getRequestContext() );
+
+                    node.getChildren().add( child );
+
+                    boolean recurse = traverse && !descriptorResult.getDependencies().isEmpty();
+                    if ( recurse )
+                    {
+                        doRecurse( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                   descriptorResult, child );
+                    }
+                }
+            }
+            else
+            {
+                DependencyNode node = args.nodes.top();
+                List<RemoteRepository> repos =
+                    getRemoteRepositories( rangeResult.getRepository( version ), repositories );
+                DefaultDependencyNode child =
+                    createDependencyNode( relocations, preManaged, rangeResult, version, d, null, repos,
+                                          args.request.getRequestContext() );
+                node.getChildren().add( child );
+            }
+        }
+    }
+
+    private void doRecurse( Args args, Results results, List<RemoteRepository> repositories,
+                            DependencySelector depSelector, DependencyManager depManager,
+                            DependencyTraverser depTraverser, VersionFilter verFilter, Dependency d,
+                            ArtifactDescriptorResult descriptorResult, DefaultDependencyNode child )
+    {
+        DefaultDependencyCollectionContext context = args.collectionContext;
+        context.set( d, descriptorResult.getManagedDependencies() );
+
+        DependencySelector childSelector = depSelector != null ? depSelector.deriveChildSelector( context ) : null;
+        DependencyManager childManager = depManager != null ? depManager.deriveChildManager( context ) : null;
+        DependencyTraverser childTraverser = depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null;
+        VersionFilter childFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null;
+
+        final List<RemoteRepository> childRepos =
+            args.ignoreRepos
+                ? repositories
+                : remoteRepositoryManager.aggregateRepositories( args.session, repositories,
+                                                                 descriptorResult.getRepositories(), true );
+
+        Object key =
+            args.pool.toKey( d.getArtifact(), childRepos, childSelector, childManager, childTraverser, childFilter );
+
+        List<DependencyNode> children = args.pool.getChildren( key );
+        if ( children == null )
+        {
+            args.pool.putChildren( key, child.getChildren() );
+
+            args.nodes.push( child );
+
+            process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager,
+                     childTraverser, childFilter );
+
+            args.nodes.pop();
+        }
+        else
+        {
+            child.setChildren( children );
+        }
+    }
+
+    private ArtifactDescriptorResult getArtifactDescriptorResult( Args args, Results results, boolean noDescriptor,
+                                                                  Dependency d,
+                                                                  ArtifactDescriptorRequest descriptorRequest )
+    {
+        return noDescriptor
+                   ? new ArtifactDescriptorResult( descriptorRequest )
+                   : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session, d, results, args );
+
+    }
+
+    private ArtifactDescriptorResult resolveCachedArtifactDescriptor( DataPool pool,
+                                                                      ArtifactDescriptorRequest descriptorRequest,
+                                                                      RepositorySystemSession session, Dependency d,
+                                                                      Results results, Args args )
+    {
+        Object key = pool.toKey( descriptorRequest );
+        ArtifactDescriptorResult descriptorResult = pool.getDescriptor( key, descriptorRequest );
+        if ( descriptorResult == null )
+        {
+            try
+            {
+                descriptorResult = descriptorReader.readArtifactDescriptor( session, descriptorRequest );
+                pool.putDescriptor( key, descriptorResult );
+            }
+            catch ( ArtifactDescriptorException e )
+            {
+                results.addException( d, e, args.nodes );
+                pool.putDescriptor( key, e );
+                return null;
+            }
+
+        }
+        else if ( descriptorResult == DataPool.NO_DESCRIPTOR )
+        {
+            return null;
+        }
+
+        return descriptorResult;
+    }
+
+    private static DefaultDependencyNode createDependencyNode( List<Artifact> relocations,
+                                                               PremanagedDependency preManaged,
+                                                               VersionRangeResult rangeResult, Version version,
+                                                               Dependency d, Collection<Artifact> aliases,
+                                                               List<RemoteRepository> repos, String requestContext )
+    {
+        DefaultDependencyNode child = new DefaultDependencyNode( d );
+        preManaged.applyTo( child );
+        child.setRelocations( relocations );
+        child.setVersionConstraint( rangeResult.getVersionConstraint() );
+        child.setVersion( version );
+        child.setAliases( aliases );
+        child.setRepositories( repos );
+        child.setRequestContext( requestContext );
+        return child;
+    }
+
+    private static DefaultDependencyNode createDependencyNode( List<Artifact> relocations,
+                                                               PremanagedDependency preManaged,
+                                                               VersionRangeResult rangeResult, Version version,
+                                                               Dependency d, ArtifactDescriptorResult descriptorResult,
+                                                               DependencyNode cycleNode )
+    {
+        DefaultDependencyNode child =
+            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult.getAliases(),
+                                  cycleNode.getRepositories(), cycleNode.getRequestContext() );
+        child.setChildren( cycleNode.getChildren() );
+        return child;
+    }
+
+    private static ArtifactDescriptorRequest createArtifactDescriptorRequest( Args args,
+                                                                              List<RemoteRepository> repositories,
+                                                                              Dependency d )
+    {
+        ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
+        descriptorRequest.setArtifact( d.getArtifact() );
+        descriptorRequest.setRepositories( repositories );
+        descriptorRequest.setRequestContext( args.request.getRequestContext() );
+        descriptorRequest.setTrace( args.trace );
+        return descriptorRequest;
+    }
+
+    private static VersionRangeRequest createVersionRangeRequest( Args args, List<RemoteRepository> repositories,
+                                                                  Dependency dependency )
+    {
+        VersionRangeRequest rangeRequest = new VersionRangeRequest();
+        rangeRequest.setArtifact( dependency.getArtifact() );
+        rangeRequest.setRepositories( repositories );
+        rangeRequest.setRequestContext( args.request.getRequestContext() );
+        rangeRequest.setTrace( args.trace );
+        return rangeRequest;
+    }
+
+    private VersionRangeResult cachedResolveRangeResult( VersionRangeRequest rangeRequest, DataPool pool,
+                                                         RepositorySystemSession session )
+        throws VersionRangeResolutionException
+    {
+        Object key = pool.toKey( rangeRequest );
+        VersionRangeResult rangeResult = pool.getConstraint( key, rangeRequest );
+        if ( rangeResult == null )
+        {
+            rangeResult = versionRangeResolver.resolveVersionRange( session, rangeRequest );
+            pool.putConstraint( key, rangeResult );
+        }
+        return rangeResult;
+    }
+
+    private static boolean isLackingDescriptor( Artifact artifact )
+    {
+        return artifact.getProperty( ArtifactProperties.LOCAL_PATH, null ) != null;
+    }
+
+    private static List<RemoteRepository> getRemoteRepositories( ArtifactRepository repository,
+                                                                 List<RemoteRepository> repositories )
+    {
+        if ( repository instanceof RemoteRepository )
+        {
+            return Collections.singletonList( (RemoteRepository) repository );
+        }
+        if ( repository != null )
+        {
+            return Collections.emptyList();
+        }
+        return repositories;
+    }
+
+    private static List<? extends Version> filterVersions( Dependency dependency, VersionRangeResult rangeResult,
+                                                           VersionFilter verFilter,
+                                                           DefaultVersionFilterContext verContext )
+        throws VersionRangeResolutionException
+    {
+        if ( rangeResult.getVersions().isEmpty() )
+        {
+            throw new VersionRangeResolutionException( rangeResult,
+                                                       "No versions available for " + dependency.getArtifact()
+                                                           + " within specified range" );
+        }
+
+        List<? extends Version> versions;
+        if ( verFilter != null && rangeResult.getVersionConstraint().getRange() != null )
+        {
+            verContext.set( dependency, rangeResult );
+            try
+            {
+                verFilter.filterVersions( verContext );
+            }
+            catch ( RepositoryException e )
+            {
+                throw new VersionRangeResolutionException( rangeResult,
+                                                           "Failed to filter versions for " + dependency.getArtifact()
+                                                               + ": " + e.getMessage(), e );
+            }
+            versions = verContext.get();
+            if ( versions.isEmpty() )
+            {
+                throw new VersionRangeResolutionException( rangeResult,
+                                                           "No acceptable versions for " + dependency.getArtifact()
+                                                               + ": " + rangeResult.getVersions() );
+            }
+        }
+        else
+        {
+            versions = rangeResult.getVersions();
+        }
+        return versions;
+    }
+
+    static class Args
+    {
+
+        final RepositorySystemSession session;
+
+        final boolean ignoreRepos;
+
+        final boolean premanagedState;
+
+        final RequestTrace trace;
+
+        final DataPool pool;
+
+        final NodeStack nodes;
+
+        final DefaultDependencyCollectionContext collectionContext;
+
+        final DefaultVersionFilterContext versionContext;
+
+        final CollectRequest request;
+
+        public Args( RepositorySystemSession session, RequestTrace trace, DataPool pool, NodeStack nodes,
+                     DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
+                     CollectRequest request )
+        {
+            this.session = session;
+            this.request = request;
+            this.ignoreRepos = session.isIgnoreArtifactDescriptorRepositories();
+            this.premanagedState = ConfigUtils.getBoolean( session, false, DependencyManagerUtils.CONFIG_PROP_VERBOSE );
+            this.trace = trace;
+            this.pool = pool;
+            this.nodes = nodes;
+            this.collectionContext = collectionContext;
+            this.versionContext = versionContext;
+        }
+
+    }
+
+    static class Results
+    {
+
+        private final CollectResult result;
+
+        final int maxExceptions;
+
+        final int maxCycles;
+
+        String errorPath;
+
+        public Results( CollectResult result, RepositorySystemSession session )
+        {
+            this.result = result;
+            this.maxExceptions = ConfigUtils.getInteger( session, 50, CONFIG_PROP_MAX_EXCEPTIONS );
+            this.maxCycles = ConfigUtils.getInteger( session, 10, CONFIG_PROP_MAX_CYCLES );
+        }
+
+        public void addException( Dependency dependency, Exception e, NodeStack nodes )
+        {
+            if ( maxExceptions < 0 || result.getExceptions().size() < maxExceptions )
+            {
+                result.addException( e );
+                if ( errorPath == null )
+                {
+                    StringBuilder buffer = new StringBuilder( 256 );
+                    for ( int i = 0; i < nodes.size(); i++ )
+                    {
+                        if ( buffer.length() > 0 )
+                        {
+                            buffer.append( " -> " );
+                        }
+                        Dependency dep = nodes.get( i ).getDependency();
+                        if ( dep != null )
+                        {
+                            buffer.append( dep.getArtifact() );
+                        }
+                    }
+                    if ( buffer.length() > 0 )
+                    {
+                        buffer.append( " -> " );
+                    }
+                    buffer.append( dependency.getArtifact() );
+                    errorPath = buffer.toString();
+                }
+            }
+        }
+
+        public void addCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
+        {
+            if ( maxCycles < 0 || result.getCycles().size() < maxCycles )
+            {
+                result.addCycle( new DefaultDependencyCycle( nodes, cycleEntry, dependency ) );
+            }
+        }
+
+    }
+
+    static class PremanagedDependency
+    {
+
+        final String premanagedVersion;
+
+        final String premanagedScope;
+
+        final Boolean premanagedOptional;
+
+        /**
+         * @since 1.1.0
+         */
+        final Collection<Exclusion> premanagedExclusions;
+
+        /**
+         * @since 1.1.0
+         */
+        final Map<String, String> premanagedProperties;
+
+        final int managedBits;
+
+        final Dependency managedDependency;
+
+        final boolean premanagedState;
+
+        PremanagedDependency( String premanagedVersion, String premanagedScope, Boolean premanagedOptional,
+                              Collection<Exclusion> premanagedExclusions, Map<String, String> premanagedProperties,
+                              int managedBits, Dependency managedDependency, boolean premanagedState )
+        {
+            this.premanagedVersion = premanagedVersion;
+            this.premanagedScope = premanagedScope;
+            this.premanagedOptional = premanagedOptional;
+            this.premanagedExclusions =
+                premanagedExclusions != null
+                    ? Collections.unmodifiableCollection( new ArrayList<Exclusion>( premanagedExclusions ) )
+                    : null;
+
+            this.premanagedProperties =
+                premanagedProperties != null
+                    ? Collections.unmodifiableMap( new HashMap<String, String>( premanagedProperties ) )
+                    : null;
+
+            this.managedBits = managedBits;
+            this.managedDependency = managedDependency;
+            this.premanagedState = premanagedState;
+        }
+
+        static PremanagedDependency create( DependencyManager depManager, Dependency dependency,
+                                            boolean disableVersionManagement, boolean premanagedState )
+        {
+            DependencyManagement depMngt = depManager != null ? depManager.manageDependency( dependency ) : null;
+
+            int managedBits = 0;
+            String premanagedVersion = null;
+            String premanagedScope = null;
+            Boolean premanagedOptional = null;
+            Collection<Exclusion> premanagedExclusions = null;
+            Map<String, String> premanagedProperties = null;
+
+            if ( depMngt != null )
+            {
+                if ( depMngt.getVersion() != null && !disableVersionManagement )
+                {
+                    Artifact artifact = dependency.getArtifact();
+                    premanagedVersion = artifact.getVersion();
+                    dependency = dependency.setArtifact( artifact.setVersion( depMngt.getVersion() ) );
+                    managedBits |= DependencyNode.MANAGED_VERSION;
+                }
+                if ( depMngt.getProperties() != null )
+                {
+                    Artifact artifact = dependency.getArtifact();
+                    premanagedProperties = artifact.getProperties();
+                    dependency = dependency.setArtifact( artifact.setProperties( depMngt.getProperties() ) );
+                    managedBits |= DependencyNode.MANAGED_PROPERTIES;
+                }
+                if ( depMngt.getScope() != null )
+                {
+                    premanagedScope = dependency.getScope();
+                    dependency = dependency.setScope( depMngt.getScope() );
+                    managedBits |= DependencyNode.MANAGED_SCOPE;
+                }
+                if ( depMngt.getOptional() != null )
+                {
+                    premanagedOptional = dependency.isOptional();
+                    dependency = dependency.setOptional( depMngt.getOptional() );
+                    managedBits |= DependencyNode.MANAGED_OPTIONAL;
+                }
+                if ( depMngt.getExclusions() != null )
+                {
+                    premanagedExclusions = dependency.getExclusions();
+                    dependency = dependency.setExclusions( depMngt.getExclusions() );
+                    managedBits |= DependencyNode.MANAGED_EXCLUSIONS;
+                }
+            }
+            return new PremanagedDependency( premanagedVersion, premanagedScope, premanagedOptional,
+                                             premanagedExclusions, premanagedProperties, managedBits, dependency,
+                                             premanagedState );
+
+        }
+
+        public void applyTo( DefaultDependencyNode child )
+        {
+            child.setManagedBits( managedBits );
+            if ( premanagedState )
+            {
+                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_VERSION, premanagedVersion );
+                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_SCOPE, premanagedScope );
+                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_OPTIONAL, premanagedOptional );
+                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_EXCLUSIONS, premanagedExclusions );
+                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_PROPERTIES, premanagedProperties );
+            }
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java
new file mode 100644
index 0000000..5ffcf67
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyCycle.java
@@ -0,0 +1,87 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+
+/**
+ * @see DefaultDependencyCollector
+ */
+final class DefaultDependencyCycle
+    implements DependencyCycle
+{
+
+    private final List<Dependency> dependencies;
+
+    private final int cycleEntry;
+
+    public DefaultDependencyCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
+    {
+        // skip root node unless it actually has a dependency or is considered the cycle entry (due to its label)
+        int offset = ( cycleEntry > 0 && nodes.get( 0 ).getDependency() == null ) ? 1 : 0;
+        Dependency[] dependencies = new Dependency[nodes.size() - offset + 1];
+        for ( int i = 0, n = dependencies.length - 1; i < n; i++ )
+        {
+            DependencyNode node = nodes.get( i + offset );
+            dependencies[i] = node.getDependency();
+            // when cycle starts at root artifact as opposed to root dependency, synthesize a dependency
+            if ( dependencies[i] == null )
+            {
+                dependencies[i] = new Dependency( node.getArtifact(), null );
+            }
+        }
+        dependencies[dependencies.length - 1] = dependency;
+        this.dependencies = Collections.unmodifiableList( Arrays.asList( dependencies ) );
+        this.cycleEntry = cycleEntry;
+    }
+
+    public List<Dependency> getPrecedingDependencies()
+    {
+        return dependencies.subList( 0, cycleEntry );
+    }
+
+    public List<Dependency> getCyclicDependencies()
+    {
+        return dependencies.subList( cycleEntry, dependencies.size() );
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        for ( int i = 0, n = dependencies.size(); i < n; i++ )
+        {
+            if ( i > 0 )
+            {
+                buffer.append( " -> " );
+            }
+            buffer.append( ArtifactIdUtils.toVersionlessId( dependencies.get( i ).getArtifact() ) );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyGraphTransformationContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyGraphTransformationContext.java
new file mode 100644
index 0000000..9eb3e2f
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDependencyGraphTransformationContext.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+
+/**
+ */
+class DefaultDependencyGraphTransformationContext
+    implements DependencyGraphTransformationContext
+{
+
+    private final RepositorySystemSession session;
+
+    private final Map<Object, Object> map;
+
+    public DefaultDependencyGraphTransformationContext( RepositorySystemSession session )
+    {
+        this.session = session;
+        this.map = new HashMap<Object, Object>();
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Object get( Object key )
+    {
+        return map.get( requireNonNull( key, "key cannot be null" ) );
+    }
+
+    public Object put( Object key, Object value )
+    {
+        requireNonNull( key, "key cannot be null" );
+        if ( value != null )
+        {
+            return map.put( key, value );
+        }
+        else
+        {
+            return map.remove( key );
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( map );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
new file mode 100644
index 0000000..48240b2
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
@@ -0,0 +1,632 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.impl.Deployer;
+import org.eclipse.aether.impl.MetadataGenerator;
+import org.eclipse.aether.impl.MetadataGeneratorFactory;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.metadata.MergeableMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.spi.connector.ArtifactUpload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.MetadataUpload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+
+/**
+ */
+@Named
+public class DefaultDeployer
+    implements Deployer, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private FileProcessor fileProcessor;
+
+    private RepositoryEventDispatcher repositoryEventDispatcher;
+
+    private RepositoryConnectorProvider repositoryConnectorProvider;
+
+    private RemoteRepositoryManager remoteRepositoryManager;
+
+    private UpdateCheckManager updateCheckManager;
+
+    private Collection<MetadataGeneratorFactory> metadataFactories = new ArrayList<MetadataGeneratorFactory>();
+
+    private SyncContextFactory syncContextFactory;
+
+    private OfflineController offlineController;
+
+    public DefaultDeployer()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultDeployer( FileProcessor fileProcessor, RepositoryEventDispatcher repositoryEventDispatcher,
+                     RepositoryConnectorProvider repositoryConnectorProvider,
+                     RemoteRepositoryManager remoteRepositoryManager, UpdateCheckManager updateCheckManager,
+                     Set<MetadataGeneratorFactory> metadataFactories, SyncContextFactory syncContextFactory,
+                     OfflineController offlineController, LoggerFactory loggerFactory )
+    {
+        setFileProcessor( fileProcessor );
+        setRepositoryEventDispatcher( repositoryEventDispatcher );
+        setRepositoryConnectorProvider( repositoryConnectorProvider );
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setUpdateCheckManager( updateCheckManager );
+        setMetadataGeneratorFactories( metadataFactories );
+        setSyncContextFactory( syncContextFactory );
+        setLoggerFactory( loggerFactory );
+        setOfflineController( offlineController );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setFileProcessor( locator.getService( FileProcessor.class ) );
+        setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
+        setRepositoryConnectorProvider( locator.getService( RepositoryConnectorProvider.class ) );
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setUpdateCheckManager( locator.getService( UpdateCheckManager.class ) );
+        setMetadataGeneratorFactories( locator.getServices( MetadataGeneratorFactory.class ) );
+        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
+        setOfflineController( locator.getService( OfflineController.class ) );
+    }
+
+    public DefaultDeployer setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultDeployer setFileProcessor( FileProcessor fileProcessor )
+    {
+        this.fileProcessor = requireNonNull( fileProcessor, "file processor cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
+    {
+        this.repositoryEventDispatcher = requireNonNull( repositoryEventDispatcher, "repository event dispatcher cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer setRepositoryConnectorProvider( RepositoryConnectorProvider repositoryConnectorProvider )
+    {
+        this.repositoryConnectorProvider = requireNonNull( repositoryConnectorProvider, "repository connector provider cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager = requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer setUpdateCheckManager( UpdateCheckManager updateCheckManager )
+    {
+        this.updateCheckManager = requireNonNull( updateCheckManager, "update check manager cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer addMetadataGeneratorFactory( MetadataGeneratorFactory factory )
+    {
+        metadataFactories.add( requireNonNull( factory, "metadata generator factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultDeployer setMetadataGeneratorFactories( Collection<MetadataGeneratorFactory> metadataFactories )
+    {
+        if ( metadataFactories == null )
+        {
+            this.metadataFactories = new ArrayList<MetadataGeneratorFactory>();
+        }
+        else
+        {
+            this.metadataFactories = metadataFactories;
+        }
+        return this;
+    }
+
+    public DefaultDeployer setSyncContextFactory( SyncContextFactory syncContextFactory )
+    {
+        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
+        return this;
+    }
+
+    public DefaultDeployer setOfflineController( OfflineController offlineController )
+    {
+        this.offlineController = requireNonNull( offlineController, "offline controller cannot be null" );
+        return this;
+    }
+
+    public DeployResult deploy( RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException
+    {
+        try
+        {
+            Utils.checkOffline( session, offlineController, request.getRepository() );
+        }
+        catch ( RepositoryOfflineException e )
+        {
+            throw new DeploymentException( "Cannot deploy while " + request.getRepository().getId() + " ("
+                + request.getRepository().getUrl() + ") is in offline mode", e );
+        }
+
+        SyncContext syncContext = syncContextFactory.newInstance( session, false );
+
+        try
+        {
+            return deploy( syncContext, session, request );
+        }
+        finally
+        {
+            syncContext.close();
+        }
+    }
+
+    private DeployResult deploy( SyncContext syncContext, RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException
+    {
+        DeployResult result = new DeployResult( request );
+
+        RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+        RemoteRepository repository = request.getRepository();
+
+        RepositoryConnector connector;
+        try
+        {
+            connector = repositoryConnectorProvider.newRepositoryConnector( session, repository );
+        }
+        catch ( NoRepositoryConnectorException e )
+        {
+            throw new DeploymentException( "Failed to deploy artifacts/metadata: " + e.getMessage(), e );
+        }
+
+        try
+        {
+            List<? extends MetadataGenerator> generators = getMetadataGenerators( session, request );
+
+            List<ArtifactUpload> artifactUploads = new ArrayList<ArtifactUpload>();
+            List<MetadataUpload> metadataUploads = new ArrayList<MetadataUpload>();
+            IdentityHashMap<Metadata, Object> processedMetadata = new IdentityHashMap<Metadata, Object>();
+
+            EventCatapult catapult = new EventCatapult( session, trace, repository, repositoryEventDispatcher );
+
+            List<Artifact> artifacts = new ArrayList<Artifact>( request.getArtifacts() );
+
+            List<Metadata> metadatas = Utils.prepareMetadata( generators, artifacts );
+
+            syncContext.acquire( artifacts, Utils.combine( request.getMetadata(), metadatas ) );
+
+            for ( Metadata metadata : metadatas )
+            {
+                upload( metadataUploads, session, metadata, repository, connector, catapult );
+                processedMetadata.put( metadata, null );
+            }
+
+            for ( int i = 0; i < artifacts.size(); i++ )
+            {
+                Artifact artifact = artifacts.get( i );
+
+                for ( MetadataGenerator generator : generators )
+                {
+                    artifact = generator.transformArtifact( artifact );
+                }
+
+                artifacts.set( i, artifact );
+
+                ArtifactUpload upload = new ArtifactUpload( artifact, artifact.getFile() );
+                upload.setTrace( trace );
+                upload.setListener( new ArtifactUploadListener( catapult, upload, logger ) );
+                artifactUploads.add( upload );
+            }
+
+            connector.put( artifactUploads, null );
+
+            for ( ArtifactUpload upload : artifactUploads )
+            {
+                if ( upload.getException() != null )
+                {
+                    throw new DeploymentException( "Failed to deploy artifacts: " + upload.getException().getMessage(),
+                                                   upload.getException() );
+                }
+                result.addArtifact( upload.getArtifact() );
+            }
+
+            metadatas = Utils.finishMetadata( generators, artifacts );
+
+            syncContext.acquire( null, metadatas );
+
+            for ( Metadata metadata : metadatas )
+            {
+                upload( metadataUploads, session, metadata, repository, connector, catapult );
+                processedMetadata.put( metadata, null );
+            }
+
+            for ( Metadata metadata : request.getMetadata() )
+            {
+                if ( !processedMetadata.containsKey( metadata ) )
+                {
+                    upload( metadataUploads, session, metadata, repository, connector, catapult );
+                    processedMetadata.put( metadata, null );
+                }
+            }
+
+            connector.put( null, metadataUploads );
+
+            for ( MetadataUpload upload : metadataUploads )
+            {
+                if ( upload.getException() != null )
+                {
+                    throw new DeploymentException( "Failed to deploy metadata: " + upload.getException().getMessage(),
+                                                   upload.getException() );
+                }
+                result.addMetadata( upload.getMetadata() );
+            }
+        }
+        finally
+        {
+            connector.close();
+        }
+
+        return result;
+    }
+
+    private List<? extends MetadataGenerator> getMetadataGenerators( RepositorySystemSession session,
+                                                                     DeployRequest request )
+    {
+        PrioritizedComponents<MetadataGeneratorFactory> factories =
+            Utils.sortMetadataGeneratorFactories( session, this.metadataFactories );
+
+        List<MetadataGenerator> generators = new ArrayList<MetadataGenerator>();
+
+        for ( PrioritizedComponent<MetadataGeneratorFactory> factory : factories.getEnabled() )
+        {
+            MetadataGenerator generator = factory.getComponent().newInstance( session, request );
+            if ( generator != null )
+            {
+                generators.add( generator );
+            }
+        }
+
+        return generators;
+    }
+
+    private void upload( Collection<MetadataUpload> metadataUploads, RepositorySystemSession session,
+                         Metadata metadata, RemoteRepository repository, RepositoryConnector connector,
+                         EventCatapult catapult )
+        throws DeploymentException
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+        File basedir = lrm.getRepository().getBasedir();
+
+        File dstFile = new File( basedir, lrm.getPathForRemoteMetadata( metadata, repository, "" ) );
+
+        if ( metadata instanceof MergeableMetadata )
+        {
+            if ( !( (MergeableMetadata) metadata ).isMerged() )
+            {
+                {
+                    RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVING );
+                    event.setTrace( catapult.getTrace() );
+                    event.setMetadata( metadata );
+                    event.setRepository( repository );
+                    repositoryEventDispatcher.dispatch( event.build() );
+
+                    event = new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADING );
+                    event.setTrace( catapult.getTrace() );
+                    event.setMetadata( metadata );
+                    event.setRepository( repository );
+                    repositoryEventDispatcher.dispatch( event.build() );
+                }
+
+                RepositoryPolicy policy = getPolicy( session, repository, metadata.getNature() );
+                MetadataDownload download = new MetadataDownload();
+                download.setMetadata( metadata );
+                download.setFile( dstFile );
+                download.setChecksumPolicy( policy.getChecksumPolicy() );
+                download.setListener( SafeTransferListener.wrap( session, logger ) );
+                download.setTrace( catapult.getTrace() );
+                connector.get( null, Arrays.asList( download ) );
+
+                Exception error = download.getException();
+
+                if ( error instanceof MetadataNotFoundException )
+                {
+                    dstFile.delete();
+                }
+
+                {
+                    RepositoryEvent.Builder event =
+                        new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADED );
+                    event.setTrace( catapult.getTrace() );
+                    event.setMetadata( metadata );
+                    event.setRepository( repository );
+                    event.setException( error );
+                    event.setFile( dstFile );
+                    repositoryEventDispatcher.dispatch( event.build() );
+
+                    event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVED );
+                    event.setTrace( catapult.getTrace() );
+                    event.setMetadata( metadata );
+                    event.setRepository( repository );
+                    event.setException( error );
+                    event.setFile( dstFile );
+                    repositoryEventDispatcher.dispatch( event.build() );
+                }
+
+                if ( error != null && !( error instanceof MetadataNotFoundException ) )
+                {
+                    throw new DeploymentException( "Failed to retrieve remote metadata " + metadata + ": "
+                        + error.getMessage(), error );
+                }
+            }
+
+            try
+            {
+                ( (MergeableMetadata) metadata ).merge( dstFile, dstFile );
+            }
+            catch ( RepositoryException e )
+            {
+                throw new DeploymentException( "Failed to update metadata " + metadata + ": " + e.getMessage(), e );
+            }
+        }
+        else
+        {
+            if ( metadata.getFile() == null )
+            {
+                throw new DeploymentException( "Failed to update metadata " + metadata + ": No file attached." );
+            }
+            try
+            {
+                fileProcessor.copy( metadata.getFile(), dstFile );
+            }
+            catch ( IOException e )
+            {
+                throw new DeploymentException( "Failed to update metadata " + metadata + ": " + e.getMessage(), e );
+            }
+        }
+
+        UpdateCheck<Metadata, MetadataTransferException> check = new UpdateCheck<Metadata, MetadataTransferException>();
+        check.setItem( metadata );
+        check.setFile( dstFile );
+        check.setRepository( repository );
+        check.setAuthoritativeRepository( repository );
+        updateCheckManager.touchMetadata( session, check );
+
+        MetadataUpload upload = new MetadataUpload( metadata, dstFile );
+        upload.setTrace( catapult.getTrace() );
+        upload.setListener( new MetadataUploadListener( catapult, upload, logger ) );
+        metadataUploads.add( upload );
+    }
+
+    private RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository,
+                                        Metadata.Nature nature )
+    {
+        boolean releases = !Metadata.Nature.SNAPSHOT.equals( nature );
+        boolean snapshots = !Metadata.Nature.RELEASE.equals( nature );
+        return remoteRepositoryManager.getPolicy( session, repository, releases, snapshots );
+    }
+
+    static final class EventCatapult
+    {
+
+        private final RepositorySystemSession session;
+
+        private final RequestTrace trace;
+
+        private final RemoteRepository repository;
+
+        private final RepositoryEventDispatcher dispatcher;
+
+        public EventCatapult( RepositorySystemSession session, RequestTrace trace, RemoteRepository repository,
+                              RepositoryEventDispatcher dispatcher )
+        {
+            this.session = session;
+            this.trace = trace;
+            this.repository = repository;
+            this.dispatcher = dispatcher;
+        }
+
+        public RepositorySystemSession getSession()
+        {
+            return session;
+        }
+
+        public RequestTrace getTrace()
+        {
+            return trace;
+        }
+
+        public void artifactDeploying( Artifact artifact, File file )
+        {
+            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DEPLOYING );
+            event.setTrace( trace );
+            event.setArtifact( artifact );
+            event.setRepository( repository );
+            event.setFile( file );
+
+            dispatcher.dispatch( event.build() );
+        }
+
+        public void artifactDeployed( Artifact artifact, File file, ArtifactTransferException exception )
+        {
+            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_DEPLOYED );
+            event.setTrace( trace );
+            event.setArtifact( artifact );
+            event.setRepository( repository );
+            event.setFile( file );
+            event.setException( exception );
+
+            dispatcher.dispatch( event.build() );
+        }
+
+        public void metadataDeploying( Metadata metadata, File file )
+        {
+            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DEPLOYING );
+            event.setTrace( trace );
+            event.setMetadata( metadata );
+            event.setRepository( repository );
+            event.setFile( file );
+
+            dispatcher.dispatch( event.build() );
+        }
+
+        public void metadataDeployed( Metadata metadata, File file, Exception exception )
+        {
+            RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DEPLOYED );
+            event.setTrace( trace );
+            event.setMetadata( metadata );
+            event.setRepository( repository );
+            event.setFile( file );
+            event.setException( exception );
+
+            dispatcher.dispatch( event.build() );
+        }
+
+    }
+
+    static final class ArtifactUploadListener
+        extends SafeTransferListener
+    {
+
+        private final EventCatapult catapult;
+
+        private final ArtifactUpload transfer;
+
+        public ArtifactUploadListener( EventCatapult catapult, ArtifactUpload transfer, Logger logger )
+        {
+            super( catapult.getSession(), logger );
+            this.catapult = catapult;
+            this.transfer = transfer;
+        }
+
+        @Override
+        public void transferInitiated( TransferEvent event )
+            throws TransferCancelledException
+        {
+            super.transferInitiated( event );
+            catapult.artifactDeploying( transfer.getArtifact(), transfer.getFile() );
+        }
+
+        @Override
+        public void transferFailed( TransferEvent event )
+        {
+            super.transferFailed( event );
+            catapult.artifactDeployed( transfer.getArtifact(), transfer.getFile(), transfer.getException() );
+        }
+
+        @Override
+        public void transferSucceeded( TransferEvent event )
+        {
+            super.transferSucceeded( event );
+            catapult.artifactDeployed( transfer.getArtifact(), transfer.getFile(), null );
+        }
+
+    }
+
+    static final class MetadataUploadListener
+        extends SafeTransferListener
+    {
+
+        private final EventCatapult catapult;
+
+        private final MetadataUpload transfer;
+
+        public MetadataUploadListener( EventCatapult catapult, MetadataUpload transfer, Logger logger )
+        {
+            super( catapult.getSession(), logger );
+            this.catapult = catapult;
+            this.transfer = transfer;
+        }
+
+        @Override
+        public void transferInitiated( TransferEvent event )
+            throws TransferCancelledException
+        {
+            super.transferInitiated( event );
+            catapult.metadataDeploying( transfer.getMetadata(), transfer.getFile() );
+        }
+
+        @Override
+        public void transferFailed( TransferEvent event )
+        {
+            super.transferFailed( event );
+            catapult.metadataDeployed( transfer.getMetadata(), transfer.getFile(), transfer.getException() );
+        }
+
+        @Override
+        public void transferSucceeded( TransferEvent event )
+        {
+            super.transferSucceeded( event );
+            catapult.metadataDeployed( transfer.getMetadata(), transfer.getFile(), null );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
new file mode 100644
index 0000000..6ba2915
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
@@ -0,0 +1,259 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import javax.inject.Named;
+
+import org.eclipse.aether.spi.io.FileProcessor;
+
+/**
+ * A utility class helping with file-based operations.
+ */
+@Named
+public class DefaultFileProcessor
+    implements FileProcessor
+{
+
+    /**
+     * Thread-safe variant of {@link File#mkdirs()}. Creates the directory named by the given abstract pathname,
+     * including any necessary but nonexistent parent directories. Note that if this operation fails it may have
+     * succeeded in creating some of the necessary parent directories.
+     * 
+     * @param directory The directory to create, may be {@code null}.
+     * @return {@code true} if and only if the directory was created, along with all necessary parent directories;
+     *         {@code false} otherwise
+     */
+    public boolean mkdirs( File directory )
+    {
+        if ( directory == null )
+        {
+            return false;
+        }
+
+        if ( directory.exists() )
+        {
+            return false;
+        }
+        if ( directory.mkdir() )
+        {
+            return true;
+        }
+
+        File canonDir;
+        try
+        {
+            canonDir = directory.getCanonicalFile();
+        }
+        catch ( IOException e )
+        {
+            return false;
+        }
+
+        File parentDir = canonDir.getParentFile();
+        return ( parentDir != null && ( mkdirs( parentDir ) || parentDir.exists() ) && canonDir.mkdir() );
+    }
+
+    public void write( File target, String data )
+        throws IOException
+    {
+        mkdirs( target.getAbsoluteFile().getParentFile() );
+
+        OutputStream out = null;
+        try
+        {
+            out = new FileOutputStream( target );
+
+            if ( data != null )
+            {
+                out.write( data.getBytes( StandardCharsets.UTF_8 ) );
+            }
+
+            out.close();
+            out = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( out != null )
+                {
+                    out.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public void write( File target, InputStream source )
+        throws IOException
+    {
+        mkdirs( target.getAbsoluteFile().getParentFile() );
+
+        OutputStream out = null;
+        try
+        {
+            out = new FileOutputStream( target );
+
+            copy( out, source, null );
+
+            out.close();
+            out = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( out != null )
+                {
+                    out.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public void copy( File source, File target )
+        throws IOException
+    {
+        copy( source, target, null );
+    }
+
+    public long copy( File source, File target, ProgressListener listener )
+        throws IOException
+    {
+        long total = 0L;
+
+        InputStream in = null;
+        OutputStream out = null;
+        try
+        {
+            in = new FileInputStream( source );
+
+            mkdirs( target.getAbsoluteFile().getParentFile() );
+
+            out = new FileOutputStream( target );
+
+            total = copy( out, in, listener );
+
+            out.close();
+            out = null;
+
+            in.close();
+            in = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( out != null )
+                {
+                    out.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+            finally
+            {
+                try
+                {
+                    if ( in != null )
+                    {
+                        in.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+        }
+
+        return total;
+    }
+
+    private long copy( OutputStream os, InputStream is, ProgressListener listener )
+        throws IOException
+    {
+        long total = 0L;
+
+        ByteBuffer buffer = ByteBuffer.allocate( 1024 * 32 );
+        byte[] array = buffer.array();
+
+        while ( true )
+        {
+            int bytes = is.read( array );
+            if ( bytes < 0 )
+            {
+                break;
+            }
+
+            os.write( array, 0, bytes );
+
+            total += bytes;
+
+            if ( listener != null && bytes > 0 )
+            {
+                try
+                {
+                    buffer.rewind();
+                    buffer.limit( bytes );
+                    listener.progressed( buffer );
+                }
+                catch ( Exception e )
+                {
+                    // too bad
+                }
+            }
+        }
+
+        return total;
+    }
+
+    public void move( File source, File target )
+        throws IOException
+    {
+        if ( !source.renameTo( target ) )
+        {
+            copy( source, target );
+
+            target.setLastModified( source.lastModified() );
+
+            source.delete();
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
new file mode 100644
index 0000000..a419fc3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
@@ -0,0 +1,376 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.Installer;
+import org.eclipse.aether.impl.MetadataGenerator;
+import org.eclipse.aether.impl.MetadataGeneratorFactory;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.metadata.MergeableMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ */
+@Named
+public class DefaultInstaller
+    implements Installer, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private FileProcessor fileProcessor;
+
+    private RepositoryEventDispatcher repositoryEventDispatcher;
+
+    private Collection<MetadataGeneratorFactory> metadataFactories = new ArrayList<MetadataGeneratorFactory>();
+
+    private SyncContextFactory syncContextFactory;
+
+    public DefaultInstaller()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultInstaller( FileProcessor fileProcessor, RepositoryEventDispatcher repositoryEventDispatcher,
+                      Set<MetadataGeneratorFactory> metadataFactories, SyncContextFactory syncContextFactory,
+                      LoggerFactory loggerFactory )
+    {
+        setFileProcessor( fileProcessor );
+        setRepositoryEventDispatcher( repositoryEventDispatcher );
+        setMetadataGeneratorFactories( metadataFactories );
+        setSyncContextFactory( syncContextFactory );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setFileProcessor( locator.getService( FileProcessor.class ) );
+        setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
+        setMetadataGeneratorFactories( locator.getServices( MetadataGeneratorFactory.class ) );
+        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
+    }
+
+    public DefaultInstaller setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultInstaller setFileProcessor( FileProcessor fileProcessor )
+    {
+        this.fileProcessor = requireNonNull( fileProcessor, "file processor cannot be null" );
+        return this;
+    }
+
+    public DefaultInstaller setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
+    {
+        this.repositoryEventDispatcher = requireNonNull( repositoryEventDispatcher, "repository event dispatcher cannot be null" );
+        return this;
+    }
+
+    public DefaultInstaller addMetadataGeneratorFactory( MetadataGeneratorFactory factory )
+    {
+        metadataFactories.add( requireNonNull( factory, "metadata generator factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultInstaller setMetadataGeneratorFactories( Collection<MetadataGeneratorFactory> metadataFactories )
+    {
+        if ( metadataFactories == null )
+        {
+            this.metadataFactories = new ArrayList<MetadataGeneratorFactory>();
+        }
+        else
+        {
+            this.metadataFactories = metadataFactories;
+        }
+        return this;
+    }
+
+    public DefaultInstaller setSyncContextFactory( SyncContextFactory syncContextFactory )
+    {
+        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
+        return this;
+    }
+
+    public InstallResult install( RepositorySystemSession session, InstallRequest request )
+        throws InstallationException
+    {
+        SyncContext syncContext = syncContextFactory.newInstance( session, false );
+
+        try
+        {
+            return install( syncContext, session, request );
+        }
+        finally
+        {
+            syncContext.close();
+        }
+    }
+
+    private InstallResult install( SyncContext syncContext, RepositorySystemSession session, InstallRequest request )
+        throws InstallationException
+    {
+        InstallResult result = new InstallResult( request );
+
+        RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+        List<? extends MetadataGenerator> generators = getMetadataGenerators( session, request );
+
+        List<Artifact> artifacts = new ArrayList<Artifact>( request.getArtifacts() );
+
+        IdentityHashMap<Metadata, Object> processedMetadata = new IdentityHashMap<Metadata, Object>();
+
+        List<Metadata> metadatas = Utils.prepareMetadata( generators, artifacts );
+
+        syncContext.acquire( artifacts, Utils.combine( request.getMetadata(), metadatas ) );
+
+        for ( Metadata metadata : metadatas )
+        {
+            install( session, trace, metadata );
+            processedMetadata.put( metadata, null );
+            result.addMetadata( metadata );
+        }
+
+        for ( int i = 0; i < artifacts.size(); i++ )
+        {
+            Artifact artifact = artifacts.get( i );
+
+            for ( MetadataGenerator generator : generators )
+            {
+                artifact = generator.transformArtifact( artifact );
+            }
+
+            artifacts.set( i, artifact );
+
+            install( session, trace, artifact );
+            result.addArtifact( artifact );
+        }
+
+        metadatas = Utils.finishMetadata( generators, artifacts );
+
+        syncContext.acquire( null, metadatas );
+
+        for ( Metadata metadata : metadatas )
+        {
+            install( session, trace, metadata );
+            processedMetadata.put( metadata, null );
+            result.addMetadata( metadata );
+        }
+
+        for ( Metadata metadata : request.getMetadata() )
+        {
+            if ( !processedMetadata.containsKey( metadata ) )
+            {
+                install( session, trace, metadata );
+                result.addMetadata( metadata );
+            }
+        }
+
+        return result;
+    }
+
+    private List<? extends MetadataGenerator> getMetadataGenerators( RepositorySystemSession session,
+                                                                     InstallRequest request )
+    {
+        PrioritizedComponents<MetadataGeneratorFactory> factories =
+            Utils.sortMetadataGeneratorFactories( session, this.metadataFactories );
+
+        List<MetadataGenerator> generators = new ArrayList<MetadataGenerator>();
+
+        for ( PrioritizedComponent<MetadataGeneratorFactory> factory : factories.getEnabled() )
+        {
+            MetadataGenerator generator = factory.getComponent().newInstance( session, request );
+            if ( generator != null )
+            {
+                generators.add( generator );
+            }
+        }
+
+        return generators;
+    }
+
+    private void install( RepositorySystemSession session, RequestTrace trace, Artifact artifact )
+        throws InstallationException
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+
+        File srcFile = artifact.getFile();
+
+        File dstFile = new File( lrm.getRepository().getBasedir(), lrm.getPathForLocalArtifact( artifact ) );
+
+        artifactInstalling( session, trace, artifact, dstFile );
+
+        Exception exception = null;
+        try
+        {
+            if ( dstFile.equals( srcFile ) )
+            {
+                throw new IllegalStateException( "cannot install " + dstFile + " to same path" );
+            }
+
+            boolean copy =
+                "pom".equals( artifact.getExtension() ) || srcFile.lastModified() != dstFile.lastModified()
+                    || srcFile.length() != dstFile.length() || !srcFile.exists();
+
+            if ( copy )
+            {
+                fileProcessor.copy( srcFile, dstFile );
+                dstFile.setLastModified( srcFile.lastModified() );
+            }
+            else
+            {
+                logger.debug( "Skipped re-installing " + srcFile + " to " + dstFile + ", seems unchanged" );
+            }
+
+            lrm.add( session, new LocalArtifactRegistration( artifact ) );
+        }
+        catch ( Exception e )
+        {
+            exception = e;
+            throw new InstallationException( "Failed to install artifact " + artifact + ": " + e.getMessage(), e );
+        }
+        finally
+        {
+            artifactInstalled( session, trace, artifact, dstFile, exception );
+        }
+    }
+
+    private void install( RepositorySystemSession session, RequestTrace trace, Metadata metadata )
+        throws InstallationException
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+
+        File dstFile = new File( lrm.getRepository().getBasedir(), lrm.getPathForLocalMetadata( metadata ) );
+
+        metadataInstalling( session, trace, metadata, dstFile );
+
+        Exception exception = null;
+        try
+        {
+            if ( metadata instanceof MergeableMetadata )
+            {
+                ( (MergeableMetadata) metadata ).merge( dstFile, dstFile );
+            }
+            else
+            {
+                if ( dstFile.equals( metadata.getFile() ) )
+                {
+                    throw new IllegalStateException( "cannot install " + dstFile + " to same path" );
+                }
+                fileProcessor.copy( metadata.getFile(), dstFile );
+            }
+
+            lrm.add( session, new LocalMetadataRegistration( metadata ) );
+        }
+        catch ( Exception e )
+        {
+            exception = e;
+            throw new InstallationException( "Failed to install metadata " + metadata + ": " + e.getMessage(), e );
+        }
+        finally
+        {
+            metadataInstalled( session, trace, metadata, dstFile, exception );
+        }
+    }
+
+    private void artifactInstalling( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                                     File dstFile )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_INSTALLING );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+        event.setRepository( session.getLocalRepositoryManager().getRepository() );
+        event.setFile( dstFile );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void artifactInstalled( RepositorySystemSession session, RequestTrace trace, Artifact artifact,
+                                    File dstFile, Exception exception )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.ARTIFACT_INSTALLED );
+        event.setTrace( trace );
+        event.setArtifact( artifact );
+        event.setRepository( session.getLocalRepositoryManager().getRepository() );
+        event.setFile( dstFile );
+        event.setException( exception );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void metadataInstalling( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                     File dstFile )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_INSTALLING );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( session.getLocalRepositoryManager().getRepository() );
+        event.setFile( dstFile );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void metadataInstalled( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                    File dstFile, Exception exception )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_INSTALLED );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( session.getLocalRepositoryManager().getRepository() );
+        event.setFile( dstFile );
+        event.setException( exception );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java
new file mode 100644
index 0000000..ea89804
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java
@@ -0,0 +1,159 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.LocalRepositoryProvider;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ */
+@Named
+public class DefaultLocalRepositoryProvider
+    implements LocalRepositoryProvider, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private Collection<LocalRepositoryManagerFactory> managerFactories = new ArrayList<LocalRepositoryManagerFactory>();
+
+    public DefaultLocalRepositoryProvider()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultLocalRepositoryProvider( Set<LocalRepositoryManagerFactory> factories, LoggerFactory loggerFactory )
+    {
+        setLocalRepositoryManagerFactories( factories );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setLocalRepositoryManagerFactories( locator.getServices( LocalRepositoryManagerFactory.class ) );
+    }
+
+    public DefaultLocalRepositoryProvider setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultLocalRepositoryProvider addLocalRepositoryManagerFactory( LocalRepositoryManagerFactory factory )
+    {
+        managerFactories.add( requireNonNull( factory, "local repository manager factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultLocalRepositoryProvider setLocalRepositoryManagerFactories( Collection<LocalRepositoryManagerFactory> factories )
+    {
+        if ( factories == null )
+        {
+            managerFactories = new ArrayList<LocalRepositoryManagerFactory>( 2 );
+        }
+        else
+        {
+            managerFactories = factories;
+        }
+        return this;
+    }
+
+    public LocalRepositoryManager newLocalRepositoryManager( RepositorySystemSession session, LocalRepository repository )
+        throws NoLocalRepositoryManagerException
+    {
+        PrioritizedComponents<LocalRepositoryManagerFactory> factories =
+            new PrioritizedComponents<LocalRepositoryManagerFactory>( session );
+        for ( LocalRepositoryManagerFactory factory : this.managerFactories )
+        {
+            factories.add( factory, factory.getPriority() );
+        }
+
+        List<NoLocalRepositoryManagerException> errors = new ArrayList<NoLocalRepositoryManagerException>();
+        for ( PrioritizedComponent<LocalRepositoryManagerFactory> factory : factories.getEnabled() )
+        {
+            try
+            {
+                LocalRepositoryManager manager = factory.getComponent().newInstance( session, repository );
+
+                if ( logger.isDebugEnabled() )
+                {
+                    StringBuilder buffer = new StringBuilder( 256 );
+                    buffer.append( "Using manager " ).append( manager.getClass().getSimpleName() );
+                    Utils.appendClassLoader( buffer, manager );
+                    buffer.append( " with priority " ).append( factory.getPriority() );
+                    buffer.append( " for " ).append( repository.getBasedir() );
+
+                    logger.debug( buffer.toString() );
+                }
+
+                return manager;
+            }
+            catch ( NoLocalRepositoryManagerException e )
+            {
+                // continue and try next factory
+                errors.add( e );
+            }
+        }
+        if ( logger.isDebugEnabled() && errors.size() > 1 )
+        {
+            String msg = "Could not obtain local repository manager for " + repository;
+            for ( Exception e : errors )
+            {
+                logger.debug( msg, e );
+            }
+        }
+
+        StringBuilder buffer = new StringBuilder( 256 );
+        if ( factories.isEmpty() )
+        {
+            buffer.append( "No local repository managers registered" );
+        }
+        else
+        {
+            buffer.append( "Cannot access " ).append( repository.getBasedir() );
+            buffer.append( " with type " ).append( repository.getContentType() );
+            buffer.append( " using the available factories " );
+            factories.list( buffer );
+        }
+
+        throw new NoLocalRepositoryManagerException( repository, buffer.toString(), errors.size() == 1 ? errors.get( 0 )
+                        : null );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java
new file mode 100644
index 0000000..f820ec9
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java
@@ -0,0 +1,635 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.impl.MetadataResolver;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.concurrency.RunnableErrorForwarder;
+import org.eclipse.aether.util.concurrency.WorkerThreadFactory;
+
+/**
+ */
+@Named
+public class DefaultMetadataResolver
+    implements MetadataResolver, Service
+{
+
+    private static final String CONFIG_PROP_THREADS = "aether.metadataResolver.threads";
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private RepositoryEventDispatcher repositoryEventDispatcher;
+
+    private UpdateCheckManager updateCheckManager;
+
+    private RepositoryConnectorProvider repositoryConnectorProvider;
+
+    private RemoteRepositoryManager remoteRepositoryManager;
+
+    private SyncContextFactory syncContextFactory;
+
+    private OfflineController offlineController;
+
+    public DefaultMetadataResolver()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultMetadataResolver( RepositoryEventDispatcher repositoryEventDispatcher,
+                             UpdateCheckManager updateCheckManager,
+                             RepositoryConnectorProvider repositoryConnectorProvider,
+                             RemoteRepositoryManager remoteRepositoryManager, SyncContextFactory syncContextFactory,
+                             OfflineController offlineController, LoggerFactory loggerFactory )
+    {
+        setRepositoryEventDispatcher( repositoryEventDispatcher );
+        setUpdateCheckManager( updateCheckManager );
+        setRepositoryConnectorProvider( repositoryConnectorProvider );
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setSyncContextFactory( syncContextFactory );
+        setOfflineController( offlineController );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
+        setUpdateCheckManager( locator.getService( UpdateCheckManager.class ) );
+        setRepositoryConnectorProvider( locator.getService( RepositoryConnectorProvider.class ) );
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
+        setOfflineController( locator.getService( OfflineController.class ) );
+    }
+
+    public DefaultMetadataResolver setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultMetadataResolver setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
+    {
+        this.repositoryEventDispatcher = requireNonNull( repositoryEventDispatcher, "repository event dispatcher cannot be null" );
+        return this;
+    }
+
+    public DefaultMetadataResolver setUpdateCheckManager( UpdateCheckManager updateCheckManager )
+    {
+        this.updateCheckManager = requireNonNull( updateCheckManager, "update check manager cannot be null" );
+        return this;
+    }
+
+    public DefaultMetadataResolver setRepositoryConnectorProvider( RepositoryConnectorProvider repositoryConnectorProvider )
+    {
+        this.repositoryConnectorProvider = requireNonNull( repositoryConnectorProvider, "repository connector provider cannot be null" );
+        return this;
+    }
+
+    public DefaultMetadataResolver setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager = requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
+        return this;
+    }
+
+    public DefaultMetadataResolver setSyncContextFactory( SyncContextFactory syncContextFactory )
+    {
+        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
+        return this;
+    }
+
+    public DefaultMetadataResolver setOfflineController( OfflineController offlineController )
+    {
+        this.offlineController = requireNonNull( offlineController, "offline controller cannot be null" );
+        return this;
+    }
+
+    public List<MetadataResult> resolveMetadata( RepositorySystemSession session,
+                                                 Collection<? extends MetadataRequest> requests )
+    {
+        SyncContext syncContext = syncContextFactory.newInstance( session, false );
+
+        try
+        {
+            Collection<Metadata> metadata = new ArrayList<Metadata>( requests.size() );
+            for ( MetadataRequest request : requests )
+            {
+                metadata.add( request.getMetadata() );
+            }
+
+            syncContext.acquire( null, metadata );
+
+            return resolve( session, requests );
+        }
+        finally
+        {
+            syncContext.close();
+        }
+    }
+
+    private List<MetadataResult> resolve( RepositorySystemSession session,
+                                          Collection<? extends MetadataRequest> requests )
+    {
+        List<MetadataResult> results = new ArrayList<MetadataResult>( requests.size() );
+
+        List<ResolveTask> tasks = new ArrayList<ResolveTask>( requests.size() );
+
+        Map<File, Long> localLastUpdates = new HashMap<File, Long>();
+
+        for ( MetadataRequest request : requests )
+        {
+            RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+            MetadataResult result = new MetadataResult( request );
+            results.add( result );
+
+            Metadata metadata = request.getMetadata();
+            RemoteRepository repository = request.getRepository();
+
+            if ( repository == null )
+            {
+                LocalRepository localRepo = session.getLocalRepositoryManager().getRepository();
+
+                metadataResolving( session, trace, metadata, localRepo );
+
+                File localFile = getLocalFile( session, metadata );
+
+                if ( localFile != null )
+                {
+                    metadata = metadata.setFile( localFile );
+                    result.setMetadata( metadata );
+                }
+                else
+                {
+                    result.setException( new MetadataNotFoundException( metadata, localRepo ) );
+                }
+
+                metadataResolved( session, trace, metadata, localRepo, result.getException() );
+                continue;
+            }
+
+            List<RemoteRepository> repositories = getEnabledSourceRepositories( repository, metadata.getNature() );
+
+            if ( repositories.isEmpty() )
+            {
+                continue;
+            }
+
+            metadataResolving( session, trace, metadata, repository );
+            LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+            LocalMetadataRequest localRequest =
+                new LocalMetadataRequest( metadata, repository, request.getRequestContext() );
+            LocalMetadataResult lrmResult = lrm.find( session, localRequest );
+
+            File metadataFile = lrmResult.getFile();
+
+            try
+            {
+                Utils.checkOffline( session, offlineController, repository );
+            }
+            catch ( RepositoryOfflineException e )
+            {
+                if ( metadataFile != null )
+                {
+                    metadata = metadata.setFile( metadataFile );
+                    result.setMetadata( metadata );
+                }
+                else
+                {
+                    String msg =
+                        "Cannot access " + repository.getId() + " (" + repository.getUrl()
+                            + ") in offline mode and the metadata " + metadata
+                            + " has not been downloaded from it before";
+                    result.setException( new MetadataNotFoundException( metadata, repository, msg, e ) );
+                }
+
+                metadataResolved( session, trace, metadata, repository, result.getException() );
+                continue;
+            }
+
+            Long localLastUpdate = null;
+            if ( request.isFavorLocalRepository() )
+            {
+                File localFile = getLocalFile( session, metadata );
+                localLastUpdate = localLastUpdates.get( localFile );
+                if ( localLastUpdate == null )
+                {
+                    localLastUpdate = localFile != null ? localFile.lastModified() : 0;
+                    localLastUpdates.put( localFile, localLastUpdate );
+                }
+            }
+
+            List<UpdateCheck<Metadata, MetadataTransferException>> checks =
+                new ArrayList<UpdateCheck<Metadata, MetadataTransferException>>();
+            Exception exception = null;
+            for ( RemoteRepository repo : repositories )
+            {
+                UpdateCheck<Metadata, MetadataTransferException> check =
+                    new UpdateCheck<Metadata, MetadataTransferException>();
+                check.setLocalLastUpdated( ( localLastUpdate != null ) ? localLastUpdate : 0 );
+                check.setItem( metadata );
+
+                // use 'main' installation file for the check (-> use requested repository)
+                File checkFile =
+                    new File(
+                              session.getLocalRepository().getBasedir(),
+                              session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata, repository,
+                                                                                            request.getRequestContext() ) );
+                check.setFile( checkFile );
+                check.setRepository( repository );
+                check.setAuthoritativeRepository( repo );
+                check.setPolicy( getPolicy( session, repo, metadata.getNature() ).getUpdatePolicy() );
+
+                if ( lrmResult.isStale() )
+                {
+                    checks.add( check );
+                }
+                else
+                {
+                    updateCheckManager.checkMetadata( session, check );
+                    if ( check.isRequired() )
+                    {
+                        checks.add( check );
+                    }
+                    else if ( exception == null )
+                    {
+                        exception = check.getException();
+                    }
+                }
+            }
+
+            if ( !checks.isEmpty() )
+            {
+                RepositoryPolicy policy = getPolicy( session, repository, metadata.getNature() );
+
+                // install path may be different from lookup path
+                File installFile =
+                    new File(
+                              session.getLocalRepository().getBasedir(),
+                              session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata,
+                                                                                            request.getRepository(),
+                                                                                            request.getRequestContext() ) );
+
+                ResolveTask task =
+                    new ResolveTask( session, trace, result, installFile, checks, policy.getChecksumPolicy() );
+                tasks.add( task );
+            }
+            else
+            {
+                result.setException( exception );
+                if ( metadataFile != null )
+                {
+                    metadata = metadata.setFile( metadataFile );
+                    result.setMetadata( metadata );
+                }
+                metadataResolved( session, trace, metadata, repository, result.getException() );
+            }
+        }
+
+        if ( !tasks.isEmpty() )
+        {
+            int threads = ConfigUtils.getInteger( session, 4, CONFIG_PROP_THREADS );
+            Executor executor = getExecutor( Math.min( tasks.size(), threads ) );
+            try
+            {
+                RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
+
+                for ( ResolveTask task : tasks )
+                {
+                    executor.execute( errorForwarder.wrap( task ) );
+                }
+
+                errorForwarder.await();
+
+                for ( ResolveTask task : tasks )
+                {
+                    task.result.setException( task.exception );
+                }
+            }
+            finally
+            {
+                shutdown( executor );
+            }
+            for ( ResolveTask task : tasks )
+            {
+                Metadata metadata = task.request.getMetadata();
+                // re-lookup metadata for resolve
+                LocalMetadataRequest localRequest =
+                    new LocalMetadataRequest( metadata, task.request.getRepository(), task.request.getRequestContext() );
+                File metadataFile = session.getLocalRepositoryManager().find( session, localRequest ).getFile();
+                if ( metadataFile != null )
+                {
+                    metadata = metadata.setFile( metadataFile );
+                    task.result.setMetadata( metadata );
+                }
+                if ( task.result.getException() == null )
+                {
+                    task.result.setUpdated( true );
+                }
+                metadataResolved( session, task.trace, metadata, task.request.getRepository(),
+                                  task.result.getException() );
+            }
+        }
+
+        return results;
+    }
+
+    private File getLocalFile( RepositorySystemSession session, Metadata metadata )
+    {
+        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
+        LocalMetadataResult localResult = lrm.find( session, new LocalMetadataRequest( metadata, null, null ) );
+        File localFile = localResult.getFile();
+        return localFile;
+    }
+
+    private List<RemoteRepository> getEnabledSourceRepositories( RemoteRepository repository, Metadata.Nature nature )
+    {
+        List<RemoteRepository> repositories = new ArrayList<RemoteRepository>();
+
+        if ( repository.isRepositoryManager() )
+        {
+            for ( RemoteRepository repo : repository.getMirroredRepositories() )
+            {
+                if ( isEnabled( repo, nature ) )
+                {
+                    repositories.add( repo );
+                }
+            }
+        }
+        else if ( isEnabled( repository, nature ) )
+        {
+            repositories.add( repository );
+        }
+
+        return repositories;
+    }
+
+    private boolean isEnabled( RemoteRepository repository, Metadata.Nature nature )
+    {
+        if ( !Metadata.Nature.SNAPSHOT.equals( nature ) && repository.getPolicy( false ).isEnabled() )
+        {
+            return true;
+        }
+        if ( !Metadata.Nature.RELEASE.equals( nature ) && repository.getPolicy( true ).isEnabled() )
+        {
+            return true;
+        }
+        return false;
+    }
+
+    private RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository,
+                                        Metadata.Nature nature )
+    {
+        boolean releases = !Metadata.Nature.SNAPSHOT.equals( nature );
+        boolean snapshots = !Metadata.Nature.RELEASE.equals( nature );
+        return remoteRepositoryManager.getPolicy( session, repository, releases, snapshots );
+    }
+
+    private void metadataResolving( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                    ArtifactRepository repository )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVING );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( repository );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void metadataResolved( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                   ArtifactRepository repository, Exception exception )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_RESOLVED );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( repository );
+        event.setException( exception );
+        event.setFile( metadata.getFile() );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void metadataDownloading( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                      ArtifactRepository repository )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADING );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( repository );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private void metadataDownloaded( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
+                                     ArtifactRepository repository, File file, Exception exception )
+    {
+        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_DOWNLOADED );
+        event.setTrace( trace );
+        event.setMetadata( metadata );
+        event.setRepository( repository );
+        event.setException( exception );
+        event.setFile( file );
+
+        repositoryEventDispatcher.dispatch( event.build() );
+    }
+
+    private Executor getExecutor( int threads )
+    {
+        if ( threads <= 1 )
+        {
+            return new Executor()
+            {
+                public void execute( Runnable command )
+                {
+                    command.run();
+                }
+            };
+        }
+        else
+        {
+            return new ThreadPoolExecutor( threads, threads, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+                                           new WorkerThreadFactory( null ) );
+        }
+    }
+
+    private void shutdown( Executor executor )
+    {
+        if ( executor instanceof ExecutorService )
+        {
+            ( (ExecutorService) executor ).shutdown();
+        }
+    }
+
+    class ResolveTask
+        implements Runnable
+    {
+
+        final RepositorySystemSession session;
+
+        final RequestTrace trace;
+
+        final MetadataResult result;
+
+        final MetadataRequest request;
+
+        final File metadataFile;
+
+        final String policy;
+
+        final List<UpdateCheck<Metadata, MetadataTransferException>> checks;
+
+        volatile MetadataTransferException exception;
+
+        public ResolveTask( RepositorySystemSession session, RequestTrace trace, MetadataResult result,
+                            File metadataFile, List<UpdateCheck<Metadata, MetadataTransferException>> checks,
+                            String policy )
+        {
+            this.session = session;
+            this.trace = trace;
+            this.result = result;
+            this.request = result.getRequest();
+            this.metadataFile = metadataFile;
+            this.policy = policy;
+            this.checks = checks;
+        }
+
+        public void run()
+        {
+            Metadata metadata = request.getMetadata();
+            RemoteRepository requestRepository = request.getRepository();
+
+            metadataDownloading( session, trace, metadata, requestRepository );
+
+            try
+            {
+                List<RemoteRepository> repositories = new ArrayList<RemoteRepository>();
+                for ( UpdateCheck<Metadata, MetadataTransferException> check : checks )
+                {
+                    repositories.add( check.getAuthoritativeRepository() );
+                }
+
+                MetadataDownload download = new MetadataDownload();
+                download.setMetadata( metadata );
+                download.setRequestContext( request.getRequestContext() );
+                download.setFile( metadataFile );
+                download.setChecksumPolicy( policy );
+                download.setRepositories( repositories );
+                download.setListener( SafeTransferListener.wrap( session, logger ) );
+                download.setTrace( trace );
+
+                RepositoryConnector connector =
+                    repositoryConnectorProvider.newRepositoryConnector( session, requestRepository );
+                try
+                {
+                    connector.get( null, Arrays.asList( download ) );
+                }
+                finally
+                {
+                    connector.close();
+                }
+
+                exception = download.getException();
+
+                if ( exception == null )
+                {
+
+                    List<String> contexts = Collections.singletonList( request.getRequestContext() );
+                    LocalMetadataRegistration registration =
+                        new LocalMetadataRegistration( metadata, requestRepository, contexts );
+
+                    session.getLocalRepositoryManager().add( session, registration );
+                }
+                else if ( request.isDeleteLocalCopyIfMissing() && exception instanceof MetadataNotFoundException )
+                {
+                    download.getFile().delete();
+                }
+            }
+            catch ( NoRepositoryConnectorException e )
+            {
+                exception = new MetadataTransferException( metadata, requestRepository, e );
+            }
+
+            /*
+             * NOTE: Touch after registration with local repo to ensure concurrent resolution is not rejected with
+             * "already updated" via session data when actual update to local repo is still pending.
+             */
+            for ( UpdateCheck<Metadata, MetadataTransferException> check : checks )
+            {
+                updateCheckManager.touchMetadata( session, check.setException( exception ) );
+            }
+
+            metadataDownloaded( session, trace, metadata, requestRepository, metadataFile, exception );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
new file mode 100644
index 0000000..938a9e1
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
@@ -0,0 +1,137 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * 
+ */
+@Named
+public class DefaultOfflineController
+    implements OfflineController, Service
+{
+
+    static final String CONFIG_PROP_OFFLINE_PROTOCOLS = "aether.offline.protocols";
+
+    static final String CONFIG_PROP_OFFLINE_HOSTS = "aether.offline.hosts";
+
+    private static final Pattern SEP = Pattern.compile( "\\s*,\\s*" );
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    public DefaultOfflineController()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultOfflineController( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    public DefaultOfflineController setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public void checkOffline( RepositorySystemSession session, RemoteRepository repository )
+        throws RepositoryOfflineException
+    {
+        if ( isOfflineProtocol( session, repository ) || isOfflineHost( session, repository ) )
+        {
+            return;
+        }
+
+        throw new RepositoryOfflineException( repository );
+    }
+
+    private boolean isOfflineProtocol( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String[] protocols = getConfig( session, CONFIG_PROP_OFFLINE_PROTOCOLS );
+        if ( protocols != null )
+        {
+            String protocol = repository.getProtocol();
+            if ( protocol.length() > 0 )
+            {
+                for ( String p : protocols )
+                {
+                    if ( p.equalsIgnoreCase( protocol ) )
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isOfflineHost( RepositorySystemSession session, RemoteRepository repository )
+    {
+        String[] hosts = getConfig( session, CONFIG_PROP_OFFLINE_HOSTS );
+        if ( hosts != null )
+        {
+            String host = repository.getHost();
+            if ( host.length() > 0 )
+            {
+                for ( String h : hosts )
+                {
+                    if ( h.equalsIgnoreCase( host ) )
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private String[] getConfig( RepositorySystemSession session, String key )
+    {
+        String value = ConfigUtils.getString( session, "", key ).trim();
+        if ( value.length() <= 0 )
+        {
+            return null;
+        }
+        return SEP.split( value );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
new file mode 100644
index 0000000..a1110b4
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
@@ -0,0 +1,391 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+import static java.util.Objects.requireNonNull;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryCache;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.util.StringUtils;
+
+/**
+ */
+@Named
+public class DefaultRemoteRepositoryManager
+    implements RemoteRepositoryManager, Service
+{
+
+    private static final class LoggedMirror
+    {
+
+        private final Object[] keys;
+
+        public LoggedMirror( RemoteRepository original, RemoteRepository mirror )
+        {
+            keys = new Object[] { mirror.getId(), mirror.getUrl(), original.getId(), original.getUrl() };
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof LoggedMirror ) )
+            {
+                return false;
+            }
+            LoggedMirror that = (LoggedMirror) obj;
+            return Arrays.equals( keys, that.keys );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Arrays.hashCode( keys );
+        }
+
+    }
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private UpdatePolicyAnalyzer updatePolicyAnalyzer;
+
+    private ChecksumPolicyProvider checksumPolicyProvider;
+
+    public DefaultRemoteRepositoryManager()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultRemoteRepositoryManager( UpdatePolicyAnalyzer updatePolicyAnalyzer,
+                                    ChecksumPolicyProvider checksumPolicyProvider, LoggerFactory loggerFactory )
+    {
+        setUpdatePolicyAnalyzer( updatePolicyAnalyzer );
+        setChecksumPolicyProvider( checksumPolicyProvider );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setUpdatePolicyAnalyzer( locator.getService( UpdatePolicyAnalyzer.class ) );
+        setChecksumPolicyProvider( locator.getService( ChecksumPolicyProvider.class ) );
+    }
+
+    public DefaultRemoteRepositoryManager setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultRemoteRepositoryManager setUpdatePolicyAnalyzer( UpdatePolicyAnalyzer updatePolicyAnalyzer )
+    {
+        this.updatePolicyAnalyzer = requireNonNull( updatePolicyAnalyzer, "update policy analyzer cannot be null" );
+        return this;
+    }
+
+    public DefaultRemoteRepositoryManager setChecksumPolicyProvider( ChecksumPolicyProvider checksumPolicyProvider )
+    {
+        this.checksumPolicyProvider = requireNonNull( checksumPolicyProvider, "checksum policy provider cannot be null" );
+        return this;
+    }
+
+    public List<RemoteRepository> aggregateRepositories( RepositorySystemSession session,
+                                                         List<RemoteRepository> dominantRepositories,
+                                                         List<RemoteRepository> recessiveRepositories,
+                                                         boolean recessiveIsRaw )
+    {
+        if ( recessiveRepositories.isEmpty() )
+        {
+            return dominantRepositories;
+        }
+
+        MirrorSelector mirrorSelector = session.getMirrorSelector();
+        AuthenticationSelector authSelector = session.getAuthenticationSelector();
+        ProxySelector proxySelector = session.getProxySelector();
+
+        List<RemoteRepository> result = new ArrayList<RemoteRepository>( dominantRepositories );
+
+        next: for ( RemoteRepository recessiveRepository : recessiveRepositories )
+        {
+            RemoteRepository repository = recessiveRepository;
+
+            if ( recessiveIsRaw )
+            {
+                RemoteRepository mirrorRepository = mirrorSelector.getMirror( recessiveRepository );
+
+                if ( mirrorRepository != null )
+                {
+                    logMirror( session, recessiveRepository, mirrorRepository );
+                    repository = mirrorRepository;
+                }
+            }
+
+            String key = getKey( repository );
+
+            for ( ListIterator<RemoteRepository> it = result.listIterator(); it.hasNext(); )
+            {
+                RemoteRepository dominantRepository = it.next();
+
+                if ( key.equals( getKey( dominantRepository ) ) )
+                {
+                    if ( !dominantRepository.getMirroredRepositories().isEmpty()
+                        && !repository.getMirroredRepositories().isEmpty() )
+                    {
+                        RemoteRepository mergedRepository = mergeMirrors( session, dominantRepository, repository );
+                        if ( mergedRepository != dominantRepository )
+                        {
+                            it.set( mergedRepository );
+                        }
+                    }
+
+                    continue next;
+                }
+            }
+
+            if ( recessiveIsRaw )
+            {
+                RemoteRepository.Builder builder = null;
+                Authentication auth = authSelector.getAuthentication( repository );
+                if ( auth != null )
+                {
+                    builder = new RemoteRepository.Builder( repository );
+                    builder.setAuthentication( auth );
+                }
+                Proxy proxy = proxySelector.getProxy( repository );
+                if ( proxy != null )
+                {
+                    if ( builder == null )
+                    {
+                        builder = new RemoteRepository.Builder( repository );
+                    }
+                    builder.setProxy( proxy );
+                }
+                if ( builder != null )
+                {
+                    repository = builder.build();
+                }
+            }
+
+            result.add( repository );
+        }
+
+        return result;
+    }
+
+    private void logMirror( RepositorySystemSession session, RemoteRepository original, RemoteRepository mirror )
+    {
+        if ( !logger.isDebugEnabled() )
+        {
+            return;
+        }
+        RepositoryCache cache = session.getCache();
+        if ( cache != null )
+        {
+            Object key = new LoggedMirror( original, mirror );
+            if ( cache.get( session, key ) != null )
+            {
+                return;
+            }
+            cache.put( session, key, Boolean.TRUE );
+        }
+        logger.debug( "Using mirror " + mirror.getId() + " (" + mirror.getUrl() + ") for " + original.getId() + " ("
+            + original.getUrl() + ")." );
+    }
+
+    private String getKey( RemoteRepository repository )
+    {
+        return repository.getId();
+    }
+
+    private RemoteRepository mergeMirrors( RepositorySystemSession session, RemoteRepository dominant,
+                                           RemoteRepository recessive )
+    {
+        RemoteRepository.Builder merged = null;
+        RepositoryPolicy releases = null, snapshots = null;
+
+        next: for ( RemoteRepository rec : recessive.getMirroredRepositories() )
+        {
+            String recKey = getKey( rec );
+
+            for ( RemoteRepository dom : dominant.getMirroredRepositories() )
+            {
+                if ( recKey.equals( getKey( dom ) ) )
+                {
+                    continue next;
+                }
+            }
+
+            if ( merged == null )
+            {
+                merged = new RemoteRepository.Builder( dominant );
+                releases = dominant.getPolicy( false );
+                snapshots = dominant.getPolicy( true );
+            }
+
+            releases = merge( session, releases, rec.getPolicy( false ), false );
+            snapshots = merge( session, snapshots, rec.getPolicy( true ), false );
+
+            merged.addMirroredRepository( rec );
+        }
+
+        if ( merged == null )
+        {
+            return dominant;
+        }
+        return merged.setReleasePolicy( releases ).setSnapshotPolicy( snapshots ).build();
+    }
+
+    public RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository, boolean releases,
+                                       boolean snapshots )
+    {
+        RepositoryPolicy policy1 = releases ? repository.getPolicy( false ) : null;
+        RepositoryPolicy policy2 = snapshots ? repository.getPolicy( true ) : null;
+        RepositoryPolicy policy = merge( session, policy1, policy2, true );
+        return policy;
+    }
+
+    private RepositoryPolicy merge( RepositorySystemSession session, RepositoryPolicy policy1,
+                                    RepositoryPolicy policy2, boolean globalPolicy )
+    {
+        RepositoryPolicy policy;
+
+        if ( policy2 == null )
+        {
+            if ( globalPolicy )
+            {
+                policy = merge( policy1, session.getUpdatePolicy(), session.getChecksumPolicy() );
+            }
+            else
+            {
+                policy = policy1;
+            }
+        }
+        else if ( policy1 == null )
+        {
+            if ( globalPolicy )
+            {
+                policy = merge( policy2, session.getUpdatePolicy(), session.getChecksumPolicy() );
+            }
+            else
+            {
+                policy = policy2;
+            }
+        }
+        else if ( !policy2.isEnabled() )
+        {
+            if ( globalPolicy )
+            {
+                policy = merge( policy1, session.getUpdatePolicy(), session.getChecksumPolicy() );
+            }
+            else
+            {
+                policy = policy1;
+            }
+        }
+        else if ( !policy1.isEnabled() )
+        {
+            if ( globalPolicy )
+            {
+                policy = merge( policy2, session.getUpdatePolicy(), session.getChecksumPolicy() );
+            }
+            else
+            {
+                policy = policy2;
+            }
+        }
+        else
+        {
+            String checksums = session.getChecksumPolicy();
+            if ( globalPolicy && !StringUtils.isEmpty( checksums ) )
+            {
+                // use global override
+            }
+            else
+            {
+                checksums =
+                    checksumPolicyProvider.getEffectiveChecksumPolicy( session, policy1.getChecksumPolicy(),
+                                                                       policy2.getChecksumPolicy() );
+            }
+
+            String updates = session.getUpdatePolicy();
+            if ( globalPolicy && !StringUtils.isEmpty( updates ) )
+            {
+                // use global override
+            }
+            else
+            {
+                updates =
+                    updatePolicyAnalyzer.getEffectiveUpdatePolicy( session, policy1.getUpdatePolicy(),
+                                                                   policy2.getUpdatePolicy() );
+            }
+
+            policy = new RepositoryPolicy( true, updates, checksums );
+        }
+
+        return policy;
+    }
+
+    private RepositoryPolicy merge( RepositoryPolicy policy, String updates, String checksums )
+    {
+        if ( policy != null )
+        {
+            if ( StringUtils.isEmpty( updates ) )
+            {
+                updates = policy.getUpdatePolicy();
+            }
+            if ( StringUtils.isEmpty( checksums ) )
+            {
+                checksums = policy.getChecksumPolicy();
+            }
+            if ( !policy.getUpdatePolicy().equals( updates ) || !policy.getChecksumPolicy().equals( checksums ) )
+            {
+                policy = new RepositoryPolicy( policy.isEnabled(), updates, checksums );
+            }
+        }
+        return policy;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
new file mode 100644
index 0000000..68f3301
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
@@ -0,0 +1,181 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+
+/**
+ */
+@Named
+public class DefaultRepositoryConnectorProvider
+    implements RepositoryConnectorProvider, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private Collection<RepositoryConnectorFactory> connectorFactories = new ArrayList<RepositoryConnectorFactory>();
+
+    public DefaultRepositoryConnectorProvider()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultRepositoryConnectorProvider( Set<RepositoryConnectorFactory> connectorFactories, LoggerFactory loggerFactory )
+    {
+        setRepositoryConnectorFactories( connectorFactories );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        connectorFactories = locator.getServices( RepositoryConnectorFactory.class );
+    }
+
+    public DefaultRepositoryConnectorProvider setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultRepositoryConnectorProvider addRepositoryConnectorFactory( RepositoryConnectorFactory factory )
+    {
+        connectorFactories.add( requireNonNull( factory, "repository connector factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultRepositoryConnectorProvider setRepositoryConnectorFactories( Collection<RepositoryConnectorFactory> factories )
+    {
+        if ( factories == null )
+        {
+            this.connectorFactories = new ArrayList<RepositoryConnectorFactory>();
+        }
+        else
+        {
+            this.connectorFactories = factories;
+        }
+        return this;
+    }
+
+    public RepositoryConnector newRepositoryConnector( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryConnectorException
+    {
+        requireNonNull( repository, "remote repository cannot be null" );
+
+        PrioritizedComponents<RepositoryConnectorFactory> factories =
+            new PrioritizedComponents<RepositoryConnectorFactory>( session );
+        for ( RepositoryConnectorFactory factory : this.connectorFactories )
+        {
+            factories.add( factory, factory.getPriority() );
+        }
+
+        List<NoRepositoryConnectorException> errors = new ArrayList<NoRepositoryConnectorException>();
+        for ( PrioritizedComponent<RepositoryConnectorFactory> factory : factories.getEnabled() )
+        {
+            try
+            {
+                RepositoryConnector connector = factory.getComponent().newInstance( session, repository );
+
+                if ( logger.isDebugEnabled() )
+                {
+                    StringBuilder buffer = new StringBuilder( 256 );
+                    buffer.append( "Using connector " ).append( connector.getClass().getSimpleName() );
+                    Utils.appendClassLoader( buffer, connector );
+                    buffer.append( " with priority " ).append( factory.getPriority() );
+                    buffer.append( " for " ).append( repository.getUrl() );
+
+                    Authentication auth = repository.getAuthentication();
+                    if ( auth != null )
+                    {
+                        buffer.append( " with " ).append( auth );
+                    }
+
+                    Proxy proxy = repository.getProxy();
+                    if ( proxy != null )
+                    {
+                        buffer.append( " via " ).append( proxy.getHost() ).append( ':' ).append( proxy.getPort() );
+
+                        auth = proxy.getAuthentication();
+                        if ( auth != null )
+                        {
+                            buffer.append( " with " ).append( auth );
+                        }
+                    }
+
+                    logger.debug( buffer.toString() );
+                }
+
+                return connector;
+            }
+            catch ( NoRepositoryConnectorException e )
+            {
+                // continue and try next factory
+                errors.add( e );
+            }
+        }
+        if ( logger.isDebugEnabled() && errors.size() > 1 )
+        {
+            String msg = "Could not obtain connector factory for " + repository;
+            for ( Exception e : errors )
+            {
+                logger.debug( msg, e );
+            }
+        }
+
+        StringBuilder buffer = new StringBuilder( 256 );
+        if ( factories.isEmpty() )
+        {
+            buffer.append( "No connector factories available" );
+        }
+        else
+        {
+            buffer.append( "Cannot access " ).append( repository.getUrl() );
+            buffer.append( " with type " ).append( repository.getContentType() );
+            buffer.append( " using the available connector factories: " );
+            factories.list( buffer );
+        }
+
+        throw new NoRepositoryConnectorException( repository, buffer.toString(), errors.size() == 1 ? errors.get( 0 )
+                        : null );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcher.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcher.java
new file mode 100644
index 0000000..9970e62
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcher.java
@@ -0,0 +1,203 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ */
+@Named
+public class DefaultRepositoryEventDispatcher
+    implements RepositoryEventDispatcher, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private Collection<RepositoryListener> listeners = new ArrayList<RepositoryListener>();
+
+    public DefaultRepositoryEventDispatcher()
+    {
+        // enables no-arg constructor
+    }
+
+    @Inject
+    DefaultRepositoryEventDispatcher( Set<RepositoryListener> listeners, LoggerFactory loggerFactory )
+    {
+        setRepositoryListeners( listeners );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public DefaultRepositoryEventDispatcher setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultRepositoryEventDispatcher addRepositoryListener( RepositoryListener listener )
+    {
+        this.listeners.add( requireNonNull( listener, "repository listener cannot be null" ) );
+        return this;
+    }
+
+    public DefaultRepositoryEventDispatcher setRepositoryListeners( Collection<RepositoryListener> listeners )
+    {
+        if ( listeners == null )
+        {
+            this.listeners = new ArrayList<RepositoryListener>();
+        }
+        else
+        {
+            this.listeners = listeners;
+        }
+        return this;
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setRepositoryListeners( locator.getServices( RepositoryListener.class ) );
+    }
+
+    public void dispatch( RepositoryEvent event )
+    {
+        if ( !listeners.isEmpty() )
+        {
+            for ( RepositoryListener listener : listeners )
+            {
+                dispatch( event, listener );
+            }
+        }
+
+        RepositoryListener listener = event.getSession().getRepositoryListener();
+
+        if ( listener != null )
+        {
+            dispatch( event, listener );
+        }
+    }
+
+    private void dispatch( RepositoryEvent event, RepositoryListener listener )
+    {
+        try
+        {
+            switch ( event.getType() )
+            {
+                case ARTIFACT_DEPLOYED:
+                    listener.artifactDeployed( event );
+                    break;
+                case ARTIFACT_DEPLOYING:
+                    listener.artifactDeploying( event );
+                    break;
+                case ARTIFACT_DESCRIPTOR_INVALID:
+                    listener.artifactDescriptorInvalid( event );
+                    break;
+                case ARTIFACT_DESCRIPTOR_MISSING:
+                    listener.artifactDescriptorMissing( event );
+                    break;
+                case ARTIFACT_DOWNLOADED:
+                    listener.artifactDownloaded( event );
+                    break;
+                case ARTIFACT_DOWNLOADING:
+                    listener.artifactDownloading( event );
+                    break;
+                case ARTIFACT_INSTALLED:
+                    listener.artifactInstalled( event );
+                    break;
+                case ARTIFACT_INSTALLING:
+                    listener.artifactInstalling( event );
+                    break;
+                case ARTIFACT_RESOLVED:
+                    listener.artifactResolved( event );
+                    break;
+                case ARTIFACT_RESOLVING:
+                    listener.artifactResolving( event );
+                    break;
+                case METADATA_DEPLOYED:
+                    listener.metadataDeployed( event );
+                    break;
+                case METADATA_DEPLOYING:
+                    listener.metadataDeploying( event );
+                    break;
+                case METADATA_DOWNLOADED:
+                    listener.metadataDownloaded( event );
+                    break;
+                case METADATA_DOWNLOADING:
+                    listener.metadataDownloading( event );
+                    break;
+                case METADATA_INSTALLED:
+                    listener.metadataInstalled( event );
+                    break;
+                case METADATA_INSTALLING:
+                    listener.metadataInstalling( event );
+                    break;
+                case METADATA_INVALID:
+                    listener.metadataInvalid( event );
+                    break;
+                case METADATA_RESOLVED:
+                    listener.metadataResolved( event );
+                    break;
+                case METADATA_RESOLVING:
+                    listener.metadataResolving( event );
+                    break;
+                default:
+                    throw new IllegalStateException( "unknown repository event type " + event.getType() );
+            }
+        }
+        catch ( Exception e )
+        {
+            logError( e, listener );
+        }
+        catch ( LinkageError e )
+        {
+            logError( e, listener );
+        }
+    }
+
+    private void logError( Throwable e, Object listener )
+    {
+        String msg =
+            "Failed to dispatch repository event to " + listener.getClass().getCanonicalName() + ": " + e.getMessage();
+
+        if ( logger.isDebugEnabled() )
+        {
+            logger.warn( msg, e );
+        }
+        else
+        {
+            logger.warn( msg );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryLayoutProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryLayoutProvider.java
new file mode 100644
index 0000000..6f424e8
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryLayoutProvider.java
@@ -0,0 +1,149 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+
+/**
+ */
+@Named
+public final class DefaultRepositoryLayoutProvider
+    implements RepositoryLayoutProvider, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private Collection<RepositoryLayoutFactory> factories = new ArrayList<RepositoryLayoutFactory>();
+
+    public DefaultRepositoryLayoutProvider()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultRepositoryLayoutProvider( Set<RepositoryLayoutFactory> layoutFactories, LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+        setRepositoryLayoutFactories( layoutFactories );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setRepositoryLayoutFactories( locator.getServices( RepositoryLayoutFactory.class ) );
+    }
+
+    public DefaultRepositoryLayoutProvider setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultRepositoryLayoutProvider addRepositoryLayoutFactory( RepositoryLayoutFactory factory )
+    {
+        factories.add( requireNonNull( factory, "layout factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultRepositoryLayoutProvider setRepositoryLayoutFactories( Collection<RepositoryLayoutFactory> factories )
+    {
+        if ( factories == null )
+        {
+            this.factories = new ArrayList<RepositoryLayoutFactory>();
+        }
+        else
+        {
+            this.factories = factories;
+        }
+        return this;
+    }
+
+    public RepositoryLayout newRepositoryLayout( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryLayoutException
+    {
+        requireNonNull( repository, "remote repository cannot be null" );
+
+        PrioritizedComponents<RepositoryLayoutFactory> factories =
+            new PrioritizedComponents<RepositoryLayoutFactory>( session );
+        for ( RepositoryLayoutFactory factory : this.factories )
+        {
+            factories.add( factory, factory.getPriority() );
+        }
+
+        List<NoRepositoryLayoutException> errors = new ArrayList<NoRepositoryLayoutException>();
+        for ( PrioritizedComponent<RepositoryLayoutFactory> factory : factories.getEnabled() )
+        {
+            try
+            {
+                RepositoryLayout layout = factory.getComponent().newInstance( session, repository );
+                return layout;
+            }
+            catch ( NoRepositoryLayoutException e )
+            {
+                // continue and try next factory
+                errors.add( e );
+            }
+        }
+        if ( logger.isDebugEnabled() && errors.size() > 1 )
+        {
+            String msg = "Could not obtain layout factory for " + repository;
+            for ( Exception e : errors )
+            {
+                logger.debug( msg, e );
+            }
+        }
+
+        StringBuilder buffer = new StringBuilder( 256 );
+        if ( factories.isEmpty() )
+        {
+            buffer.append( "No layout factories registered" );
+        }
+        else
+        {
+            buffer.append( "Cannot access " ).append( repository.getUrl() );
+            buffer.append( " with type " ).append( repository.getContentType() );
+            buffer.append( " using the available layout factories: " );
+            factories.list( buffer );
+        }
+
+        throw new NoRepositoryLayoutException( repository, buffer.toString(), errors.size() == 1 ? errors.get( 0 )
+                        : null );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
new file mode 100644
index 0000000..4269494
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositorySystem.java
@@ -0,0 +1,446 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeployResult;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyVisitor;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.ArtifactResolver;
+import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.Deployer;
+import org.eclipse.aether.impl.Installer;
+import org.eclipse.aether.impl.LocalRepositoryProvider;
+import org.eclipse.aether.impl.MetadataResolver;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResolutionException;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.util.graph.visitor.FilteringDependencyVisitor;
+import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
+
+/**
+ */
+@Named
+public class DefaultRepositorySystem
+    implements RepositorySystem, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private VersionResolver versionResolver;
+
+    private VersionRangeResolver versionRangeResolver;
+
+    private ArtifactResolver artifactResolver;
+
+    private MetadataResolver metadataResolver;
+
+    private ArtifactDescriptorReader artifactDescriptorReader;
+
+    private DependencyCollector dependencyCollector;
+
+    private Installer installer;
+
+    private Deployer deployer;
+
+    private LocalRepositoryProvider localRepositoryProvider;
+
+    private SyncContextFactory syncContextFactory;
+
+    private RemoteRepositoryManager remoteRepositoryManager;
+
+    public DefaultRepositorySystem()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultRepositorySystem( VersionResolver versionResolver, VersionRangeResolver versionRangeResolver,
+                             ArtifactResolver artifactResolver, MetadataResolver metadataResolver,
+                             ArtifactDescriptorReader artifactDescriptorReader,
+                             DependencyCollector dependencyCollector, Installer installer, Deployer deployer,
+                             LocalRepositoryProvider localRepositoryProvider, SyncContextFactory syncContextFactory,
+                             RemoteRepositoryManager remoteRepositoryManager, LoggerFactory loggerFactory )
+    {
+        setVersionResolver( versionResolver );
+        setVersionRangeResolver( versionRangeResolver );
+        setArtifactResolver( artifactResolver );
+        setMetadataResolver( metadataResolver );
+        setArtifactDescriptorReader( artifactDescriptorReader );
+        setDependencyCollector( dependencyCollector );
+        setInstaller( installer );
+        setDeployer( deployer );
+        setLocalRepositoryProvider( localRepositoryProvider );
+        setSyncContextFactory( syncContextFactory );
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setVersionResolver( locator.getService( VersionResolver.class ) );
+        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
+        setArtifactResolver( locator.getService( ArtifactResolver.class ) );
+        setMetadataResolver( locator.getService( MetadataResolver.class ) );
+        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
+        setDependencyCollector( locator.getService( DependencyCollector.class ) );
+        setInstaller( locator.getService( Installer.class ) );
+        setDeployer( locator.getService( Deployer.class ) );
+        setLocalRepositoryProvider( locator.getService( LocalRepositoryProvider.class ) );
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
+    }
+
+    public DefaultRepositorySystem setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultRepositorySystem setVersionResolver( VersionResolver versionResolver )
+    {
+        this.versionResolver = requireNonNull( versionResolver, "version resolver cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
+    {
+        this.versionRangeResolver = requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setArtifactResolver( ArtifactResolver artifactResolver )
+    {
+        this.artifactResolver = requireNonNull( artifactResolver, "artifact resolver cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setMetadataResolver( MetadataResolver metadataResolver )
+    {
+        this.metadataResolver = requireNonNull( metadataResolver, "metadata resolver cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
+    {
+        this.artifactDescriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setDependencyCollector( DependencyCollector dependencyCollector )
+    {
+        this.dependencyCollector = requireNonNull( dependencyCollector, "dependency collector cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setInstaller( Installer installer )
+    {
+        this.installer = requireNonNull( installer, "installer cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setDeployer( Deployer deployer )
+    {
+        this.deployer = requireNonNull( deployer, "deployer cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setLocalRepositoryProvider( LocalRepositoryProvider localRepositoryProvider )
+    {
+        this.localRepositoryProvider = requireNonNull( localRepositoryProvider, "local repository provider cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setSyncContextFactory( SyncContextFactory syncContextFactory )
+    {
+        this.syncContextFactory = requireNonNull( syncContextFactory, "sync context factory cannot be null" );
+        return this;
+    }
+
+    public DefaultRepositorySystem setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager = requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
+        return this;
+    }
+
+    public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException
+    {
+        validateSession( session );
+        return versionResolver.resolveVersion( session, request );
+    }
+
+    public VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException
+    {
+        validateSession( session );
+        return versionRangeResolver.resolveVersionRange( session, request );
+    }
+
+    public ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session,
+                                                            ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException
+    {
+        validateSession( session );
+        return artifactDescriptorReader.readArtifactDescriptor( session, request );
+    }
+
+    public ArtifactResult resolveArtifact( RepositorySystemSession session, ArtifactRequest request )
+        throws ArtifactResolutionException
+    {
+        validateSession( session );
+        return artifactResolver.resolveArtifact( session, request );
+    }
+
+    public List<ArtifactResult> resolveArtifacts( RepositorySystemSession session,
+                                                  Collection<? extends ArtifactRequest> requests )
+        throws ArtifactResolutionException
+    {
+        validateSession( session );
+        return artifactResolver.resolveArtifacts( session, requests );
+    }
+
+    public List<MetadataResult> resolveMetadata( RepositorySystemSession session,
+                                                 Collection<? extends MetadataRequest> requests )
+    {
+        validateSession( session );
+        return metadataResolver.resolveMetadata( session, requests );
+    }
+
+    public CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
+        throws DependencyCollectionException
+    {
+        validateSession( session );
+        return dependencyCollector.collectDependencies( session, request );
+    }
+
+    public DependencyResult resolveDependencies( RepositorySystemSession session, DependencyRequest request )
+        throws DependencyResolutionException
+    {
+        validateSession( session );
+
+        RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
+
+        DependencyResult result = new DependencyResult( request );
+
+        DependencyCollectionException dce = null;
+        ArtifactResolutionException are = null;
+
+        if ( request.getRoot() != null )
+        {
+            result.setRoot( request.getRoot() );
+        }
+        else if ( request.getCollectRequest() != null )
+        {
+            CollectResult collectResult;
+            try
+            {
+                request.getCollectRequest().setTrace( trace );
+                collectResult = dependencyCollector.collectDependencies( session, request.getCollectRequest() );
+            }
+            catch ( DependencyCollectionException e )
+            {
+                dce = e;
+                collectResult = e.getResult();
+            }
+            result.setRoot( collectResult.getRoot() );
+            result.setCycles( collectResult.getCycles() );
+            result.setCollectExceptions( collectResult.getExceptions() );
+        }
+        else
+        {
+            throw new NullPointerException( "dependency node and collect request cannot be null" );
+        }
+
+        ArtifactRequestBuilder builder = new ArtifactRequestBuilder( trace );
+        DependencyFilter filter = request.getFilter();
+        DependencyVisitor visitor = ( filter != null ) ? new FilteringDependencyVisitor( builder, filter ) : builder;
+        visitor = new TreeDependencyVisitor( visitor );
+
+        if ( result.getRoot() != null )
+        {
+            result.getRoot().accept( visitor );
+        }
+
+        List<ArtifactRequest> requests = builder.getRequests();
+
+        List<ArtifactResult> results;
+        try
+        {
+            results = artifactResolver.resolveArtifacts( session, requests );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            are = e;
+            results = e.getResults();
+        }
+        result.setArtifactResults( results );
+
+        updateNodesWithResolvedArtifacts( results );
+
+        if ( dce != null )
+        {
+            throw new DependencyResolutionException( result, dce );
+        }
+        else if ( are != null )
+        {
+            throw new DependencyResolutionException( result, are );
+        }
+
+        return result;
+    }
+
+    private void updateNodesWithResolvedArtifacts( List<ArtifactResult> results )
+    {
+        for ( ArtifactResult result : results )
+        {
+            Artifact artifact = result.getArtifact();
+            if ( artifact != null )
+            {
+                result.getRequest().getDependencyNode().setArtifact( artifact );
+            }
+        }
+    }
+
+    public InstallResult install( RepositorySystemSession session, InstallRequest request )
+        throws InstallationException
+    {
+        validateSession( session );
+        return installer.install( session, request );
+    }
+
+    public DeployResult deploy( RepositorySystemSession session, DeployRequest request )
+        throws DeploymentException
+    {
+        validateSession( session );
+        return deployer.deploy( session, request );
+    }
+
+    public LocalRepositoryManager newLocalRepositoryManager( RepositorySystemSession session,
+                                                             LocalRepository localRepository )
+    {
+        try
+        {
+            return localRepositoryProvider.newLocalRepositoryManager( session, localRepository );
+        }
+        catch ( NoLocalRepositoryManagerException e )
+        {
+            throw new IllegalArgumentException( e.getMessage(), e );
+        }
+    }
+
+    public SyncContext newSyncContext( RepositorySystemSession session, boolean shared )
+    {
+        validateSession( session );
+        return syncContextFactory.newInstance( session, shared );
+    }
+
+    public List<RemoteRepository> newResolutionRepositories( RepositorySystemSession session,
+                                                             List<RemoteRepository> repositories )
+    {
+        validateSession( session );
+        repositories =
+            remoteRepositoryManager.aggregateRepositories( session, new ArrayList<RemoteRepository>(), repositories,
+                                                           true );
+        return repositories;
+    }
+
+    public RemoteRepository newDeploymentRepository( RepositorySystemSession session, RemoteRepository repository )
+    {
+        validateSession( session );
+        RemoteRepository.Builder builder = new RemoteRepository.Builder( repository );
+        Authentication auth = session.getAuthenticationSelector().getAuthentication( repository );
+        builder.setAuthentication( auth );
+        Proxy proxy = session.getProxySelector().getProxy( repository );
+        builder.setProxy( proxy );
+        return builder.build();
+    }
+
+    private void validateSession( RepositorySystemSession session )
+    {
+        requireNonNull( session, "repository system session cannot be null" );
+        invalidSession( session.getLocalRepositoryManager(), "local repository manager" );
+        invalidSession( session.getSystemProperties(), "system properties" );
+        invalidSession( session.getUserProperties(), "user properties" );
+        invalidSession( session.getConfigProperties(), "config properties" );
+        invalidSession( session.getMirrorSelector(), "mirror selector" );
+        invalidSession( session.getProxySelector(), "proxy selector" );
+        invalidSession( session.getAuthenticationSelector(), "authentication selector" );
+        invalidSession( session.getArtifactTypeRegistry(), "artifact type registry" );
+        invalidSession( session.getData(), "data" );
+    }
+
+    private void invalidSession( Object obj, String name )
+    {
+        requireNonNull( obj, "repository system session's " + name + " cannot be null" );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
new file mode 100644
index 0000000..69fdbc6
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultSyncContextFactory.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * A factory to create synchronization contexts. This default implementation actually does not provide any real
+ * synchronization but merely completes the repository system.
+ */
+@Named
+public class DefaultSyncContextFactory
+    implements SyncContextFactory
+{
+
+    public SyncContext newInstance( RepositorySystemSession session, boolean shared )
+    {
+        return new DefaultSyncContext();
+    }
+
+    static class DefaultSyncContext
+        implements SyncContext
+    {
+
+        public void acquire( Collection<? extends Artifact> artifact, Collection<? extends Metadata> metadata )
+        {
+        }
+
+        public void close()
+        {
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTransporterProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTransporterProvider.java
new file mode 100644
index 0000000..7779a6f
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTransporterProvider.java
@@ -0,0 +1,157 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterProvider;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ */
+@Named
+public final class DefaultTransporterProvider
+    implements TransporterProvider, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private Collection<TransporterFactory> factories = new ArrayList<TransporterFactory>();
+
+    public DefaultTransporterProvider()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultTransporterProvider( Set<TransporterFactory> transporterFactories, LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+        setTransporterFactories( transporterFactories );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setTransporterFactories( locator.getServices( TransporterFactory.class ) );
+    }
+
+    public DefaultTransporterProvider setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultTransporterProvider addTransporterFactory( TransporterFactory factory )
+    {
+        factories.add( requireNonNull( factory, "transporter factory cannot be null" ) );
+        return this;
+    }
+
+    public DefaultTransporterProvider setTransporterFactories( Collection<TransporterFactory> factories )
+    {
+        if ( factories == null )
+        {
+            this.factories = new ArrayList<TransporterFactory>();
+        }
+        else
+        {
+            this.factories = factories;
+        }
+        return this;
+    }
+
+    public Transporter newTransporter( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException
+    {
+        requireNonNull( repository, "remote repository cannot be null" );
+
+        PrioritizedComponents<TransporterFactory> factories = new PrioritizedComponents<TransporterFactory>( session );
+        for ( TransporterFactory factory : this.factories )
+        {
+            factories.add( factory, factory.getPriority() );
+        }
+
+        List<NoTransporterException> errors = new ArrayList<NoTransporterException>();
+        for ( PrioritizedComponent<TransporterFactory> factory : factories.getEnabled() )
+        {
+            try
+            {
+                Transporter transporter = factory.getComponent().newInstance( session, repository );
+
+                if ( logger.isDebugEnabled() )
+                {
+                    StringBuilder buffer = new StringBuilder( 256 );
+                    buffer.append( "Using transporter " ).append( transporter.getClass().getSimpleName() );
+                    Utils.appendClassLoader( buffer, transporter );
+                    buffer.append( " with priority " ).append( factory.getPriority() );
+                    buffer.append( " for " ).append( repository.getUrl() );
+                    logger.debug( buffer.toString() );
+                }
+
+                return transporter;
+            }
+            catch ( NoTransporterException e )
+            {
+                // continue and try next factory
+                errors.add( e );
+            }
+        }
+        if ( logger.isDebugEnabled() && errors.size() > 1 )
+        {
+            String msg = "Could not obtain transporter factory for " + repository;
+            for ( Exception e : errors )
+            {
+                logger.debug( msg, e );
+            }
+        }
+
+        StringBuilder buffer = new StringBuilder( 256 );
+        if ( factories.isEmpty() )
+        {
+            buffer.append( "No transporter factories registered" );
+        }
+        else
+        {
+            buffer.append( "Cannot access " ).append( repository.getUrl() );
+            buffer.append( " using the registered transporter factories: " );
+            factories.list( buffer );
+        }
+
+        throw new NoTransporterException( repository, buffer.toString(), errors.size() == 1 ? errors.get( 0 ) : null );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
new file mode 100644
index 0000000..f7827a1
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
@@ -0,0 +1,618 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SessionData;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ */
+@Named
+public class DefaultUpdateCheckManager
+    implements UpdateCheckManager, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private UpdatePolicyAnalyzer updatePolicyAnalyzer;
+
+    private static final String UPDATED_KEY_SUFFIX = ".lastUpdated";
+
+    private static final String ERROR_KEY_SUFFIX = ".error";
+
+    private static final String NOT_FOUND = "";
+
+    private static final String SESSION_CHECKS = "updateCheckManager.checks";
+
+    static final String CONFIG_PROP_SESSION_STATE = "aether.updateCheckManager.sessionState";
+
+    private static final int STATE_ENABLED = 0;
+
+    private static final int STATE_BYPASS = 1;
+
+    private static final int STATE_DISABLED = 2;
+
+    public DefaultUpdateCheckManager()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultUpdateCheckManager( UpdatePolicyAnalyzer updatePolicyAnalyzer, LoggerFactory loggerFactory )
+    {
+        setUpdatePolicyAnalyzer( updatePolicyAnalyzer );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setUpdatePolicyAnalyzer( locator.getService( UpdatePolicyAnalyzer.class ) );
+    }
+
+    public DefaultUpdateCheckManager setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public DefaultUpdateCheckManager setUpdatePolicyAnalyzer( UpdatePolicyAnalyzer updatePolicyAnalyzer )
+    {
+        this.updatePolicyAnalyzer = requireNonNull( updatePolicyAnalyzer, "update policy analyzer cannot be null" );
+        return this;
+    }
+
+    public void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
+    {
+        if ( check.getLocalLastUpdated() != 0
+            && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem()
+                    + ", locally installed artifact up-to-date." );
+            }
+
+            check.setRequired( false );
+            return;
+        }
+
+        Artifact artifact = check.getItem();
+        RemoteRepository repository = check.getRepository();
+
+        File artifactFile = requireNonNull( check.getFile(), String.format( "The artifact '%s' has no file attached", artifact ) );
+
+        boolean fileExists = check.isFileValid() && artifactFile.exists();
+
+        File touchFile = getTouchFile( artifact, artifactFile );
+        Properties props = read( touchFile );
+
+        String updateKey = getUpdateKey( session, artifactFile, repository );
+        String dataKey = getDataKey( artifact, artifactFile, repository );
+
+        String error = getError( props, dataKey );
+
+        long lastUpdated;
+        if ( error == null )
+        {
+            if ( fileExists )
+            {
+                // last update was successful
+                lastUpdated = artifactFile.lastModified();
+            }
+            else
+            {
+                // this is the first attempt ever
+                lastUpdated = 0L;
+            }
+        }
+        else if ( error.length() <= 0 )
+        {
+            // artifact did not exist
+            lastUpdated = getLastUpdated( props, dataKey );
+        }
+        else
+        {
+            // artifact could not be transferred
+            String transferKey = getTransferKey( session, artifact, artifactFile, repository );
+            lastUpdated = getLastUpdated( props, transferKey );
+        }
+
+        if ( lastUpdated == 0L )
+        {
+            check.setRequired( true );
+        }
+        else if ( isAlreadyUpdated( session, updateKey ) )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem()
+                    + ", already updated during this session." );
+            }
+
+            check.setRequired( false );
+            if ( error != null )
+            {
+                check.setException( newException( error, artifact, repository ) );
+            }
+        }
+        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
+        {
+            check.setRequired( true );
+        }
+        else if ( fileExists )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem() + ", locally cached artifact up-to-date." );
+            }
+
+            check.setRequired( false );
+        }
+        else
+        {
+            int errorPolicy = Utils.getPolicy( session, artifact, repository );
+            int cacheFlag = getCacheFlag( error );
+            if ( ( errorPolicy & cacheFlag ) != 0 )
+            {
+                check.setRequired( false );
+                check.setException( newException( error, artifact, repository ) );
+            }
+            else
+            {
+                check.setRequired( true );
+            }
+        }
+    }
+
+    private static int getCacheFlag( String error )
+    {
+        if ( error == null || error.length() <= 0 )
+        {
+            return ResolutionErrorPolicy.CACHE_NOT_FOUND;
+        }
+        else
+        {
+            return ResolutionErrorPolicy.CACHE_TRANSFER_ERROR;
+        }
+    }
+
+    private ArtifactTransferException newException( String error, Artifact artifact, RemoteRepository repository )
+    {
+        if ( error == null || error.length() <= 0 )
+        {
+            return new ArtifactNotFoundException( artifact, repository, "Failure to find " + artifact + " in "
+                + repository.getUrl() + " was cached in the local repository, "
+                + "resolution will not be reattempted until the update interval of " + repository.getId()
+                + " has elapsed or updates are forced", true );
+        }
+        else
+        {
+            return new ArtifactTransferException( artifact, repository, "Failure to transfer " + artifact + " from "
+                + repository.getUrl() + " was cached in the local repository, "
+                + "resolution will not be reattempted until the update interval of " + repository.getId()
+                + " has elapsed or updates are forced. Original error: " + error, true );
+        }
+    }
+
+    public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
+    {
+        if ( check.getLocalLastUpdated() != 0
+            && !isUpdatedRequired( session, check.getLocalLastUpdated(), check.getPolicy() ) )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem()
+                    + ", locally installed metadata up-to-date." );
+            }
+
+            check.setRequired( false );
+            return;
+        }
+
+        Metadata metadata = check.getItem();
+        RemoteRepository repository = check.getRepository();
+
+        File metadataFile = requireNonNull( check.getFile(), String.format( "The metadata '%s' has no file attached", metadata ) );
+
+        boolean fileExists = check.isFileValid() && metadataFile.exists();
+
+        File touchFile = getTouchFile( metadata, metadataFile );
+        Properties props = read( touchFile );
+
+        String updateKey = getUpdateKey( session, metadataFile, repository );
+        String dataKey = getDataKey( metadata, metadataFile, check.getAuthoritativeRepository() );
+
+        String error = getError( props, dataKey );
+
+        long lastUpdated;
+        if ( error == null )
+        {
+            if ( fileExists )
+            {
+                // last update was successful
+                lastUpdated = getLastUpdated( props, dataKey );
+            }
+            else
+            {
+                // this is the first attempt ever
+                lastUpdated = 0L;
+            }
+        }
+        else if ( error.length() <= 0 )
+        {
+            // metadata did not exist
+            lastUpdated = getLastUpdated( props, dataKey );
+        }
+        else
+        {
+            // metadata could not be transferred
+            String transferKey = getTransferKey( session, metadata, metadataFile, repository );
+            lastUpdated = getLastUpdated( props, transferKey );
+        }
+
+        if ( lastUpdated == 0L )
+        {
+            check.setRequired( true );
+        }
+        else if ( isAlreadyUpdated( session, updateKey ) )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem()
+                    + ", already updated during this session." );
+            }
+
+            check.setRequired( false );
+            if ( error != null )
+            {
+                check.setException( newException( error, metadata, repository ) );
+            }
+        }
+        else if ( isUpdatedRequired( session, lastUpdated, check.getPolicy() ) )
+        {
+            check.setRequired( true );
+        }
+        else if ( fileExists )
+        {
+            if ( logger.isDebugEnabled() )
+            {
+                logger.debug( "Skipped remote request for " + check.getItem() + ", locally cached metadata up-to-date." );
+            }
+
+            check.setRequired( false );
+        }
+        else
+        {
+            int errorPolicy = Utils.getPolicy( session, metadata, repository );
+            int cacheFlag = getCacheFlag( error );
+            if ( ( errorPolicy & cacheFlag ) != 0 )
+            {
+                check.setRequired( false );
+                check.setException( newException( error, metadata, repository ) );
+            }
+            else
+            {
+                check.setRequired( true );
+            }
+        }
+    }
+
+    private MetadataTransferException newException( String error, Metadata metadata, RemoteRepository repository )
+    {
+        if ( error == null || error.length() <= 0 )
+        {
+            return new MetadataNotFoundException( metadata, repository, "Failure to find " + metadata + " in "
+                + repository.getUrl() + " was cached in the local repository, "
+                + "resolution will not be reattempted until the update interval of " + repository.getId()
+                + " has elapsed or updates are forced", true );
+        }
+        else
+        {
+            return new MetadataTransferException( metadata, repository, "Failure to transfer " + metadata + " from "
+                + repository.getUrl() + " was cached in the local repository, "
+                + "resolution will not be reattempted until the update interval of " + repository.getId()
+                + " has elapsed or updates are forced. Original error: " + error, true );
+        }
+    }
+
+    private long getLastUpdated( Properties props, String key )
+    {
+        String value = props.getProperty( key + UPDATED_KEY_SUFFIX, "" );
+        try
+        {
+            return ( value.length() > 0 ) ? Long.parseLong( value ) : 1;
+        }
+        catch ( NumberFormatException e )
+        {
+            logger.debug( "Cannot parse lastUpdated date: \'" + value + "\'. Ignoring.", e );
+            return 1;
+        }
+    }
+
+    private String getError( Properties props, String key )
+    {
+        return props.getProperty( key + ERROR_KEY_SUFFIX );
+    }
+
+    private File getTouchFile( Artifact artifact, File artifactFile )
+    {
+        return new File( artifactFile.getPath() + ".lastUpdated" );
+    }
+
+    private File getTouchFile( Metadata metadata, File metadataFile )
+    {
+        return new File( metadataFile.getParent(), "resolver-status.properties" );
+    }
+
+    private String getDataKey( Artifact artifact, File artifactFile, RemoteRepository repository )
+    {
+        Set<String> mirroredUrls = Collections.emptySet();
+        if ( repository.isRepositoryManager() )
+        {
+            mirroredUrls = new TreeSet<String>();
+            for ( RemoteRepository mirroredRepository : repository.getMirroredRepositories() )
+            {
+                mirroredUrls.add( normalizeRepoUrl( mirroredRepository.getUrl() ) );
+            }
+        }
+
+        StringBuilder buffer = new StringBuilder( 1024 );
+
+        buffer.append( normalizeRepoUrl( repository.getUrl() ) );
+        for ( String mirroredUrl : mirroredUrls )
+        {
+            buffer.append( '+' ).append( mirroredUrl );
+        }
+
+        return buffer.toString();
+    }
+
+    private String getTransferKey( RepositorySystemSession session, Artifact artifact, File artifactFile,
+                                   RemoteRepository repository )
+    {
+        return getRepoKey( session, repository );
+    }
+
+    private String getDataKey( Metadata metadata, File metadataFile, RemoteRepository repository )
+    {
+        return metadataFile.getName();
+    }
+
+    private String getTransferKey( RepositorySystemSession session, Metadata metadata, File metadataFile,
+                                   RemoteRepository repository )
+    {
+        return metadataFile.getName() + '/' + getRepoKey( session, repository );
+    }
+
+    private String getRepoKey( RepositorySystemSession session, RemoteRepository repository )
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+
+        Proxy proxy = repository.getProxy();
+        if ( proxy != null )
+        {
+            buffer.append( AuthenticationDigest.forProxy( session, repository ) ).append( '@' );
+            buffer.append( proxy.getHost() ).append( ':' ).append( proxy.getPort() ).append( '>' );
+        }
+
+        buffer.append( AuthenticationDigest.forRepository( session, repository ) ).append( '@' );
+
+        buffer.append( repository.getContentType() ).append( '-' );
+        buffer.append( repository.getId() ).append( '-' );
+        buffer.append( normalizeRepoUrl( repository.getUrl() ) );
+
+        return buffer.toString();
+    }
+
+    private String normalizeRepoUrl( String url )
+    {
+        String result = url;
+        if ( url != null && url.length() > 0 && !url.endsWith( "/" ) )
+        {
+            result = url + '/';
+        }
+        return result;
+    }
+
+    private String getUpdateKey( RepositorySystemSession session, File file, RemoteRepository repository )
+    {
+        return file.getAbsolutePath() + '|' + getRepoKey( session, repository );
+    }
+
+    private int getSessionState( RepositorySystemSession session )
+    {
+        String mode = ConfigUtils.getString( session, "true", CONFIG_PROP_SESSION_STATE );
+        if ( Boolean.parseBoolean( mode ) )
+        {
+            // perform update check at most once per session, regardless of update policy
+            return STATE_ENABLED;
+        }
+        else if ( "bypass".equalsIgnoreCase( mode ) )
+        {
+            // evaluate update policy but record update in session to prevent potential future checks
+            return STATE_BYPASS;
+        }
+        else
+        {
+            // no session state at all, always evaluate update policy
+            return STATE_DISABLED;
+        }
+    }
+
+    private boolean isAlreadyUpdated( RepositorySystemSession session, Object updateKey )
+    {
+        if ( getSessionState( session ) >= STATE_BYPASS )
+        {
+            return false;
+        }
+        SessionData data = session.getData();
+        Object checkedFiles = data.get( SESSION_CHECKS );
+        if ( !( checkedFiles instanceof Map ) )
+        {
+            return false;
+        }
+        return ( (Map<?, ?>) checkedFiles ).containsKey( updateKey );
+    }
+
+    @SuppressWarnings( "unchecked" )
+    private void setUpdated( RepositorySystemSession session, Object updateKey )
+    {
+        if ( getSessionState( session ) >= STATE_DISABLED )
+        {
+            return;
+        }
+        SessionData data = session.getData();
+        Object checkedFiles = data.get( SESSION_CHECKS );
+        while ( !( checkedFiles instanceof Map ) )
+        {
+            Object old = checkedFiles;
+            checkedFiles = new ConcurrentHashMap<Object, Object>( 256 );
+            if ( data.set( SESSION_CHECKS, old, checkedFiles ) )
+            {
+                break;
+            }
+            checkedFiles = data.get( SESSION_CHECKS );
+        }
+        ( (Map<Object, Boolean>) checkedFiles ).put( updateKey, Boolean.TRUE );
+    }
+
+    private boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy )
+    {
+        return updatePolicyAnalyzer.isUpdatedRequired( session, lastModified, policy );
+    }
+
+    private Properties read( File touchFile )
+    {
+        Properties props = new TrackingFileManager().setLogger( logger ).read( touchFile );
+        return ( props != null ) ? props : new Properties();
+    }
+
+    public void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
+    {
+        Artifact artifact = check.getItem();
+        File artifactFile = check.getFile();
+        File touchFile = getTouchFile( artifact, artifactFile );
+
+        String updateKey = getUpdateKey( session, artifactFile, check.getRepository() );
+        String dataKey = getDataKey( artifact, artifactFile, check.getAuthoritativeRepository() );
+        String transferKey = getTransferKey( session, artifact, artifactFile, check.getRepository() );
+
+        setUpdated( session, updateKey );
+        Properties props = write( touchFile, dataKey, transferKey, check.getException() );
+
+        if ( artifactFile.exists() && !hasErrors( props ) )
+        {
+            touchFile.delete();
+        }
+    }
+
+    private boolean hasErrors( Properties props )
+    {
+        for ( Object key : props.keySet() )
+        {
+            if ( key.toString().endsWith( ERROR_KEY_SUFFIX ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
+    {
+        Metadata metadata = check.getItem();
+        File metadataFile = check.getFile();
+        File touchFile = getTouchFile( metadata, metadataFile );
+
+        String updateKey = getUpdateKey( session, metadataFile, check.getRepository() );
+        String dataKey = getDataKey( metadata, metadataFile, check.getAuthoritativeRepository() );
+        String transferKey = getTransferKey( session, metadata, metadataFile, check.getRepository() );
+
+        setUpdated( session, updateKey );
+        write( touchFile, dataKey, transferKey, check.getException() );
+    }
+
+    private Properties write( File touchFile, String dataKey, String transferKey, Exception error )
+    {
+        Map<String, String> updates = new HashMap<String, String>();
+
+        String timestamp = Long.toString( System.currentTimeMillis() );
+
+        if ( error == null )
+        {
+            updates.put( dataKey + ERROR_KEY_SUFFIX, null );
+            updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
+            updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
+        }
+        else if ( error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException )
+        {
+            updates.put( dataKey + ERROR_KEY_SUFFIX, NOT_FOUND );
+            updates.put( dataKey + UPDATED_KEY_SUFFIX, timestamp );
+            updates.put( transferKey + UPDATED_KEY_SUFFIX, null );
+        }
+        else
+        {
+            String msg = error.getMessage();
+            if ( msg == null || msg.length() <= 0 )
+            {
+                msg = error.getClass().getSimpleName();
+            }
+            updates.put( dataKey + ERROR_KEY_SUFFIX, msg );
+            updates.put( dataKey + UPDATED_KEY_SUFFIX, null );
+            updates.put( transferKey + UPDATED_KEY_SUFFIX, timestamp );
+        }
+
+        return new TrackingFileManager().setLogger( logger ).update( touchFile, updates );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzer.java
new file mode 100644
index 0000000..c2cdd83
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzer.java
@@ -0,0 +1,158 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Calendar;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ */
+@Named
+public class DefaultUpdatePolicyAnalyzer
+    implements UpdatePolicyAnalyzer, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    public DefaultUpdatePolicyAnalyzer()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    DefaultUpdatePolicyAnalyzer( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    public DefaultUpdatePolicyAnalyzer setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, getClass() );
+        return this;
+    }
+
+    public String getEffectiveUpdatePolicy( RepositorySystemSession session, String policy1, String policy2 )
+    {
+        return ordinalOfUpdatePolicy( policy1 ) < ordinalOfUpdatePolicy( policy2 ) ? policy1 : policy2;
+    }
+
+    private int ordinalOfUpdatePolicy( String policy )
+    {
+        if ( RepositoryPolicy.UPDATE_POLICY_DAILY.equals( policy ) )
+        {
+            return 1440;
+        }
+        else if ( RepositoryPolicy.UPDATE_POLICY_ALWAYS.equals( policy ) )
+        {
+            return 0;
+        }
+        else if ( policy != null && policy.startsWith( RepositoryPolicy.UPDATE_POLICY_INTERVAL ) )
+        {
+            return getMinutes( policy );
+        }
+        else
+        {
+            // assume "never"
+            return Integer.MAX_VALUE;
+        }
+    }
+
+    public boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy )
+    {
+        boolean checkForUpdates;
+
+        if ( policy == null )
+        {
+            policy = "";
+        }
+
+        if ( RepositoryPolicy.UPDATE_POLICY_ALWAYS.equals( policy ) )
+        {
+            checkForUpdates = true;
+        }
+        else if ( RepositoryPolicy.UPDATE_POLICY_DAILY.equals( policy ) )
+        {
+            Calendar cal = Calendar.getInstance();
+            cal.set( Calendar.HOUR_OF_DAY, 0 );
+            cal.set( Calendar.MINUTE, 0 );
+            cal.set( Calendar.SECOND, 0 );
+            cal.set( Calendar.MILLISECOND, 0 );
+
+            checkForUpdates = cal.getTimeInMillis() > lastModified;
+        }
+        else if ( policy.startsWith( RepositoryPolicy.UPDATE_POLICY_INTERVAL ) )
+        {
+            int minutes = getMinutes( policy );
+
+            Calendar cal = Calendar.getInstance();
+            cal.add( Calendar.MINUTE, -minutes );
+
+            checkForUpdates = cal.getTimeInMillis() > lastModified;
+        }
+        else
+        {
+            // assume "never"
+            checkForUpdates = false;
+
+            if ( !RepositoryPolicy.UPDATE_POLICY_NEVER.equals( policy ) )
+            {
+                logger.warn( "Unknown repository update policy '" + policy + "', assuming '"
+                    + RepositoryPolicy.UPDATE_POLICY_NEVER + "'" );
+            }
+        }
+
+        return checkForUpdates;
+    }
+
+    private int getMinutes( String policy )
+    {
+        int minutes;
+        try
+        {
+            String s = policy.substring( RepositoryPolicy.UPDATE_POLICY_INTERVAL.length() + 1 );
+            minutes = Integer.valueOf( s );
+        }
+        catch ( RuntimeException e )
+        {
+            minutes = 24 * 60;
+
+            logger.warn( "Non-parseable repository update policy '" + policy + "', assuming '"
+                + RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":1440'" );
+        }
+        return minutes;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultVersionFilterContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultVersionFilterContext.java
new file mode 100644
index 0000000..1ce4437
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultVersionFilterContext.java
@@ -0,0 +1,217 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * @see DefaultDependencyCollector
+ */
+final class DefaultVersionFilterContext
+    implements VersionFilter.VersionFilterContext
+{
+
+    private final Iterator<Version> EMPTY = Collections.<Version>emptySet().iterator();
+
+    private final RepositorySystemSession session;
+
+    private Dependency dependency;
+
+    VersionRangeResult result;
+
+    int count;
+
+    byte[] deleted = new byte[64];
+
+    public DefaultVersionFilterContext( RepositorySystemSession session )
+    {
+        this.session = session;
+    }
+
+    public void set( Dependency dependency, VersionRangeResult result )
+    {
+        this.dependency = dependency;
+        this.result = result;
+        count = result.getVersions().size();
+        if ( deleted.length < count )
+        {
+            deleted = new byte[count];
+        }
+        else
+        {
+            for ( int i = count - 1; i >= 0; i-- )
+            {
+                deleted[i] = (byte) 0;
+            }
+        }
+    }
+
+    public List<Version> get()
+    {
+        if ( count == result.getVersions().size() )
+        {
+            return result.getVersions();
+        }
+        if ( count <= 1 )
+        {
+            if ( count <= 0 )
+            {
+                return Collections.emptyList();
+            }
+            return Collections.singletonList( iterator().next() );
+        }
+        List<Version> versions = new ArrayList<Version>( count );
+        for ( Version version : this )
+        {
+            versions.add( version );
+        }
+        return versions;
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public VersionConstraint getVersionConstraint()
+    {
+        return result.getVersionConstraint();
+    }
+
+    public int getCount()
+    {
+        return count;
+    }
+
+    public ArtifactRepository getRepository( Version version )
+    {
+        return result.getRepository( version );
+    }
+
+    public List<RemoteRepository> getRepositories()
+    {
+        return Collections.unmodifiableList( result.getRequest().getRepositories() );
+    }
+
+    public Iterator<Version> iterator()
+    {
+        return ( count > 0 ) ? new VersionIterator() : EMPTY;
+    }
+
+    @Override
+    public String toString()
+    {
+        return dependency + " " + result.getVersions();
+    }
+
+    private class VersionIterator
+        implements Iterator<Version>
+    {
+
+        private final List<Version> versions;
+
+        private final int size;
+
+        private int count;
+
+        private int index;
+
+        private int next;
+
+        public VersionIterator()
+        {
+            count = DefaultVersionFilterContext.this.count;
+            index = -1;
+            next = 0;
+            versions = result.getVersions();
+            size = versions.size();
+            advance();
+        }
+
+        private void advance()
+        {
+            for ( next = index + 1; next < size && deleted[next] != (byte) 0; next++ )
+            {
+                // just advancing index
+            }
+        }
+
+        public boolean hasNext()
+        {
+            return next < size;
+        }
+
+        public Version next()
+        {
+            if ( count != DefaultVersionFilterContext.this.count )
+            {
+                throw new ConcurrentModificationException();
+            }
+            if ( next >= size )
+            {
+                throw new NoSuchElementException();
+            }
+            index = next;
+            advance();
+            return versions.get( index );
+        }
+
+        public void remove()
+        {
+            if ( count != DefaultVersionFilterContext.this.count )
+            {
+                throw new ConcurrentModificationException();
+            }
+            if ( index < 0 || deleted[index] == (byte) 1 )
+            {
+                throw new IllegalStateException();
+            }
+            deleted[index] = (byte) 1;
+            count = --DefaultVersionFilterContext.this.count;
+        }
+
+        @Override
+        public String toString()
+        {
+            return ( index < 0 ) ? "null" : String.valueOf( versions.get( index ) );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
new file mode 100644
index 0000000..327ec3d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -0,0 +1,222 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+import java.util.Properties;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * These are implementation details for enhanced local repository manager, subject to change without prior notice.
+ * Repositories from which a cached artifact was resolved are tracked in a properties file named
+ * <code>_remote.repositories</code>, with content key as filename&gt;repo_id and value as empty string. If a file has
+ * been installed in the repository, but not downloaded from a remote repository, it is tracked as empty repository id
+ * and always resolved. For example:
+ * 
+ * <pre>
+ * artifact-1.0.pom>=
+ * artifact-1.0.jar>=
+ * artifact-1.0.pom>central=
+ * artifact-1.0.jar>central=
+ * artifact-1.0.zip>central=
+ * artifact-1.0-classifier.zip>central=
+ * artifact-1.0.pom>my_repo_id=
+ * </pre>
+ * 
+ * @see EnhancedLocalRepositoryManagerFactory
+ */
+class EnhancedLocalRepositoryManager
+    extends SimpleLocalRepositoryManager
+{
+
+    private static final String LOCAL_REPO_ID = "";
+
+    private final String trackingFilename;
+
+    private final TrackingFileManager trackingFileManager;
+
+    public EnhancedLocalRepositoryManager( File basedir, RepositorySystemSession session )
+    {
+        super( basedir, "enhanced" );
+        String filename = ConfigUtils.getString( session, "", "aether.enhancedLocalRepository.trackingFilename" );
+        if ( filename.length() <= 0 || filename.contains( "/" ) || filename.contains( "\\" )
+            || filename.contains( ".." ) )
+        {
+            filename = "_remote.repositories";
+        }
+        trackingFilename = filename;
+        trackingFileManager = new TrackingFileManager();
+    }
+
+    @Override
+    public EnhancedLocalRepositoryManager setLogger( Logger logger )
+    {
+        super.setLogger( logger );
+        trackingFileManager.setLogger( logger );
+        return this;
+    }
+
+    @Override
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    {
+        String path = getPathForArtifact( request.getArtifact(), false );
+        File file = new File( getRepository().getBasedir(), path );
+
+        LocalArtifactResult result = new LocalArtifactResult( request );
+
+        if ( file.isFile() )
+        {
+            result.setFile( file );
+
+            Properties props = readRepos( file );
+
+            if ( props.get( getKey( file, LOCAL_REPO_ID ) ) != null )
+            {
+                // artifact installed into the local repo is always accepted
+                result.setAvailable( true );
+            }
+            else
+            {
+                String context = request.getContext();
+                for ( RemoteRepository repository : request.getRepositories() )
+                {
+                    if ( props.get( getKey( file, getRepositoryKey( repository, context ) ) ) != null )
+                    {
+                        // artifact downloaded from remote repository is accepted only downloaded from request
+                        // repositories
+                        result.setAvailable( true );
+                        result.setRepository( repository );
+                        break;
+                    }
+                }
+                if ( !result.isAvailable() && !isTracked( props, file ) )
+                {
+                    /*
+                     * NOTE: The artifact is present but not tracked at all, for inter-op with simple local repo, assume
+                     * the artifact was locally installed.
+                     */
+                    result.setAvailable( true );
+                }
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+    {
+        Collection<String> repositories;
+        if ( request.getRepository() == null )
+        {
+            repositories = Collections.singleton( LOCAL_REPO_ID );
+        }
+        else
+        {
+            repositories = getRepositoryKeys( request.getRepository(), request.getContexts() );
+        }
+        addArtifact( request.getArtifact(), repositories, request.getRepository() == null );
+    }
+
+    private Collection<String> getRepositoryKeys( RemoteRepository repository, Collection<String> contexts )
+    {
+        Collection<String> keys = new HashSet<String>();
+
+        if ( contexts != null )
+        {
+            for ( String context : contexts )
+            {
+                keys.add( getRepositoryKey( repository, context ) );
+            }
+        }
+
+        return keys;
+    }
+
+    private void addArtifact( Artifact artifact, Collection<String> repositories, boolean local )
+    {
+        String path = getPathForArtifact( requireNonNull( artifact, "artifact cannot be null" ), local );
+        File file = new File( getRepository().getBasedir(), path );
+        addRepo( file, repositories );
+    }
+
+    private Properties readRepos( File artifactFile )
+    {
+        File trackingFile = getTrackingFile( artifactFile );
+
+        Properties props = trackingFileManager.read( trackingFile );
+
+        return ( props != null ) ? props : new Properties();
+    }
+
+    private void addRepo( File artifactFile, Collection<String> repositories )
+    {
+        Map<String, String> updates = new HashMap<String, String>();
+        for ( String repository : repositories )
+        {
+            updates.put( getKey( artifactFile, repository ), "" );
+        }
+
+        File trackingFile = getTrackingFile( artifactFile );
+
+        trackingFileManager.update( trackingFile, updates );
+    }
+
+    private File getTrackingFile( File artifactFile )
+    {
+        return new File( artifactFile.getParentFile(), trackingFilename );
+    }
+
+    private String getKey( File file, String repository )
+    {
+        return file.getName() + '>' + repository;
+    }
+
+    private boolean isTracked( Properties props, File file )
+    {
+        if ( props != null )
+        {
+            String keyPrefix = file.getName() + '>';
+            for ( Object key : props.keySet() )
+            {
+                if ( key.toString().startsWith( keyPrefix ) )
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
new file mode 100644
index 0000000..904c840
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
@@ -0,0 +1,104 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ * Creates enhanced local repository managers for repository types {@code "default"} or {@code "" (automatic)}. Enhanced
+ * local repository manager is built upon the classical Maven 2.0 local repository structure but additionally keeps
+ * track of from what repositories a cached artifact was resolved. Resolution of locally cached artifacts will be
+ * rejected in case the current resolution request does not match the known source repositories of an artifact, thereby
+ * emulating physically separated artifact caches per remote repository.
+ */
+@Named( "enhanced" )
+public class EnhancedLocalRepositoryManagerFactory
+    implements LocalRepositoryManagerFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private float priority = 10.0f;
+
+    public EnhancedLocalRepositoryManagerFactory()
+    {
+        // enable no-arg constructor
+    }
+
+    @Inject
+    EnhancedLocalRepositoryManagerFactory( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public LocalRepositoryManager newInstance( RepositorySystemSession session, LocalRepository repository )
+        throws NoLocalRepositoryManagerException
+    {
+        if ( "".equals( repository.getContentType() ) || "default".equals( repository.getContentType() ) )
+        {
+            return new EnhancedLocalRepositoryManager( repository.getBasedir(), session ).setLogger( logger );
+        }
+        else
+        {
+            throw new NoLocalRepositoryManagerException( repository );
+        }
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    public EnhancedLocalRepositoryManagerFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, EnhancedLocalRepositoryManager.class );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public EnhancedLocalRepositoryManagerFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FailChecksumPolicy.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FailChecksumPolicy.java
new file mode 100644
index 0000000..4f3de45
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FailChecksumPolicy.java
@@ -0,0 +1,43 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.TransferResource;
+
+/**
+ * Implements {@link org.eclipse.aether.repository.RepositoryPolicy#CHECKSUM_POLICY_FAIL}.
+ */
+final class FailChecksumPolicy
+    extends AbstractChecksumPolicy
+{
+
+    public FailChecksumPolicy( LoggerFactory loggerFactory, TransferResource resource )
+    {
+        super( loggerFactory, resource );
+    }
+
+    public boolean onTransferChecksumFailure( ChecksumFailureException error )
+    {
+        return false;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LoggerFactoryProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LoggerFactoryProvider.java
new file mode 100644
index 0000000..3d46490
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LoggerFactoryProvider.java
@@ -0,0 +1,64 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ * Helps Sisu-based applications to pick the right logger factory depending on the classpath.
+ */
+@Named
+@Singleton
+public class LoggerFactoryProvider
+    implements Provider<LoggerFactory>
+{
+
+    @Inject
+    @Named( "slf4j" )
+    private Provider<LoggerFactory> slf4j;
+
+    public LoggerFactory get()
+    {
+        try
+        {
+            LoggerFactory factory = slf4j.get();
+            if ( factory != null )
+            {
+                return factory;
+            }
+        }
+        catch ( LinkageError e )
+        {
+            // fall through
+        }
+        catch ( RuntimeException e )
+        {
+            // fall through
+        }
+        return NullLoggerFactory.INSTANCE;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
new file mode 100644
index 0000000..9202c4b
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactory.java
@@ -0,0 +1,186 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * Provides a Maven-2 repository layout for repositories with content type {@code "default"}.
+ */
+@Named( "maven2" )
+public final class Maven2RepositoryLayoutFactory
+    implements RepositoryLayoutFactory
+{
+
+    static final String CONFIG_PROP_SIGNATURE_CHECKSUMS = "aether.checksums.forSignature";
+
+    private float priority;
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public Maven2RepositoryLayoutFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public RepositoryLayout newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryLayoutException
+    {
+        if ( !"default".equals( repository.getContentType() ) )
+        {
+            throw new NoRepositoryLayoutException( repository );
+        }
+        boolean forSignature = ConfigUtils.getBoolean( session, false, CONFIG_PROP_SIGNATURE_CHECKSUMS );
+        return forSignature ? Maven2RepositoryLayout.INSTANCE : Maven2RepositoryLayoutEx.INSTANCE;
+    }
+
+    private static class Maven2RepositoryLayout
+        implements RepositoryLayout
+    {
+
+        public static final RepositoryLayout INSTANCE = new Maven2RepositoryLayout();
+
+        private URI toUri( String path )
+        {
+            try
+            {
+                return new URI( null, null, path, null );
+            }
+            catch ( URISyntaxException e )
+            {
+                throw new IllegalStateException( e );
+            }
+        }
+
+        public URI getLocation( Artifact artifact, boolean upload )
+        {
+            StringBuilder path = new StringBuilder( 128 );
+
+            path.append( artifact.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+            path.append( artifact.getArtifactId() ).append( '/' );
+
+            path.append( artifact.getBaseVersion() ).append( '/' );
+
+            path.append( artifact.getArtifactId() ).append( '-' ).append( artifact.getVersion() );
+
+            if ( artifact.getClassifier().length() > 0 )
+            {
+                path.append( '-' ).append( artifact.getClassifier() );
+            }
+
+            if ( artifact.getExtension().length() > 0 )
+            {
+                path.append( '.' ).append( artifact.getExtension() );
+            }
+
+            return toUri( path.toString() );
+        }
+
+        public URI getLocation( Metadata metadata, boolean upload )
+        {
+            StringBuilder path = new StringBuilder( 128 );
+
+            if ( metadata.getGroupId().length() > 0 )
+            {
+                path.append( metadata.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+                if ( metadata.getArtifactId().length() > 0 )
+                {
+                    path.append( metadata.getArtifactId() ).append( '/' );
+
+                    if ( metadata.getVersion().length() > 0 )
+                    {
+                        path.append( metadata.getVersion() ).append( '/' );
+                    }
+                }
+            }
+
+            path.append( metadata.getType() );
+
+            return toUri( path.toString() );
+        }
+
+        public List<Checksum> getChecksums( Artifact artifact, boolean upload, URI location )
+        {
+            return getChecksums( location );
+        }
+
+        public List<Checksum> getChecksums( Metadata metadata, boolean upload, URI location )
+        {
+            return getChecksums( location );
+        }
+
+        private List<Checksum> getChecksums( URI location )
+        {
+            return Arrays.asList( Checksum.forLocation( location, "SHA-1" ), Checksum.forLocation( location, "MD5" ) );
+        }
+
+    }
+
+    private static class Maven2RepositoryLayoutEx
+        extends Maven2RepositoryLayout
+    {
+
+        public static final RepositoryLayout INSTANCE = new Maven2RepositoryLayoutEx();
+
+        @Override
+        public List<Checksum> getChecksums( Artifact artifact, boolean upload, URI location )
+        {
+            if ( isSignature( artifact.getExtension() ) )
+            {
+                return Collections.emptyList();
+            }
+            return super.getChecksums( artifact, upload, location );
+        }
+
+        private boolean isSignature( String extension )
+        {
+            return extension.endsWith( ".asc" );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java
new file mode 100644
index 0000000..b0e0cd3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/NodeStack.java
@@ -0,0 +1,124 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * @see DefaultDependencyCollector
+ */
+final class NodeStack
+{
+
+    private DependencyNode[] nodes = new DependencyNode[96];
+
+    private int size;
+
+    public DependencyNode top()
+    {
+        if ( size <= 0 )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        return nodes[size - 1];
+    }
+
+    public void push( DependencyNode node )
+    {
+        if ( size >= nodes.length )
+        {
+            DependencyNode[] tmp = new DependencyNode[size + 64];
+            System.arraycopy( nodes, 0, tmp, 0, nodes.length );
+            nodes = tmp;
+        }
+        nodes[size++] = node;
+    }
+
+    public void pop()
+    {
+        if ( size <= 0 )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        size--;
+    }
+
+    public int find( Artifact artifact )
+    {
+        for ( int i = size - 1; i >= 0; i-- )
+        {
+            DependencyNode node = nodes[i];
+
+            Artifact a = node.getArtifact();
+            if ( a == null )
+            {
+                break;
+            }
+
+            if ( !a.getArtifactId().equals( artifact.getArtifactId() ) )
+            {
+                continue;
+            }
+            if ( !a.getGroupId().equals( artifact.getGroupId() ) )
+            {
+                continue;
+            }
+            if ( !a.getExtension().equals( artifact.getExtension() ) )
+            {
+                continue;
+            }
+            if ( !a.getClassifier().equals( artifact.getClassifier() ) )
+            {
+                continue;
+            }
+            /*
+             * NOTE: While a:1 and a:2 are technically different artifacts, we want to consider the path a:2 -> b:2 ->
+             * a:1 a cycle in the current context. The artifacts themselves might not form a cycle but their producing
+             * projects surely do. Furthermore, conflict resolution will always have to consider a:1 a loser (otherwise
+             * its ancestor a:2 would get pruned and so would a:1) so there is no point in building the sub graph of
+             * a:1.
+             */
+
+            return i;
+        }
+
+        return -1;
+    }
+
+    public int size()
+    {
+        return size;
+    }
+
+    public DependencyNode get( int index )
+    {
+        return nodes[index];
+    }
+
+    @Override
+    public String toString()
+    {
+        return Arrays.toString( nodes );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ObjectPool.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ObjectPool.java
new file mode 100644
index 0000000..2307f7f
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/ObjectPool.java
@@ -0,0 +1,52 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Pool of immutable object instances, used to avoid excessive memory consumption of (dirty) dependency graph which
+ * tends to have many duplicate artifacts/dependencies.
+ */
+class ObjectPool<T>
+{
+
+    private final Map<Object, Reference<T>> objects = new WeakHashMap<Object, Reference<T>>( 256 );
+
+    public synchronized T intern( T object )
+    {
+        Reference<T> pooledRef = objects.get( object );
+        if ( pooledRef != null )
+        {
+            T pooled = pooledRef.get();
+            if ( pooled != null )
+            {
+                return pooled;
+            }
+        }
+
+        objects.put( object, new WeakReference<T>( object ) );
+        return object;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponent.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponent.java
new file mode 100644
index 0000000..fc9ebeb
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponent.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+final class PrioritizedComponent<T>
+    implements Comparable<PrioritizedComponent<?>>
+{
+
+    private final T component;
+
+    private final Class<?> type;
+
+    private final float priority;
+
+    private final int index;
+
+    public PrioritizedComponent( T component, Class<?> type, float priority, int index )
+    {
+        this.component = component;
+        this.type = type;
+        this.priority = priority;
+        this.index = index;
+    }
+
+    public T getComponent()
+    {
+        return component;
+    }
+
+    public Class<?> getType()
+    {
+        return type;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    public boolean isDisabled()
+    {
+        return Float.isNaN( priority );
+    }
+
+    public int compareTo( PrioritizedComponent<?> o )
+    {
+        int rel = ( isDisabled() ? 1 : 0 ) - ( o.isDisabled() ? 1 : 0 );
+        if ( rel == 0 )
+        {
+            rel = Float.compare( o.priority, priority );
+            if ( rel == 0 )
+            {
+                rel = index - o.index;
+            }
+        }
+        return rel;
+    }
+
+    @Override
+    public String toString()
+    {
+        return priority + " (#" + index + "): " + String.valueOf( component );
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponents.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponents.java
new file mode 100644
index 0000000..3ec5613
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/PrioritizedComponents.java
@@ -0,0 +1,156 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * Helps to sort pluggable components by their priority.
+ */
+final class PrioritizedComponents<T>
+{
+
+    private static final String FACTORY_SUFFIX = "Factory";
+
+    private final Map<?, ?> configProps;
+
+    private final boolean useInsertionOrder;
+
+    private final List<PrioritizedComponent<T>> components;
+
+    private int firstDisabled;
+
+    public PrioritizedComponents( RepositorySystemSession session )
+    {
+        this( session.getConfigProperties() );
+    }
+
+    PrioritizedComponents( Map<?, ?> configurationProperties )
+    {
+        configProps = configurationProperties;
+        useInsertionOrder =
+            ConfigUtils.getBoolean( configProps, ConfigurationProperties.DEFAULT_IMPLICIT_PRIORITIES,
+                                    ConfigurationProperties.IMPLICIT_PRIORITIES );
+        components = new ArrayList<PrioritizedComponent<T>>();
+        firstDisabled = 0;
+    }
+
+    public void add( T component, float priority )
+    {
+        Class<?> type = getImplClass( component );
+        int index = components.size();
+        priority = useInsertionOrder ? -index : ConfigUtils.getFloat( configProps, priority, getConfigKeys( type ) );
+        PrioritizedComponent<T> pc = new PrioritizedComponent<T>( component, type, priority, index );
+
+        if ( !useInsertionOrder )
+        {
+            index = Collections.binarySearch( components, pc );
+            if ( index < 0 )
+            {
+                index = -index - 1;
+            }
+            else
+            {
+                index++;
+            }
+        }
+        components.add( index, pc );
+
+        if ( index <= firstDisabled && !pc.isDisabled() )
+        {
+            firstDisabled++;
+        }
+    }
+
+    private static Class<?> getImplClass( Object component )
+    {
+        Class<?> type = component.getClass();
+        // detect and ignore CGLIB-based proxy classes employed by Guice for AOP (cf. BytecodeGen.newEnhancer)
+        int idx = type.getName().indexOf( "$$" );
+        if ( idx >= 0 )
+        {
+            Class<?> base = type.getSuperclass();
+            if ( base != null && idx == base.getName().length() && type.getName().startsWith( base.getName() ) )
+            {
+                type = base;
+            }
+        }
+        return type;
+    }
+
+    static String[] getConfigKeys( Class<?> type )
+    {
+        List<String> keys = new ArrayList<String>();
+        keys.add( ConfigurationProperties.PREFIX_PRIORITY + type.getName() );
+        String sn = type.getSimpleName();
+        keys.add( ConfigurationProperties.PREFIX_PRIORITY + sn );
+        if ( sn.endsWith( FACTORY_SUFFIX ) )
+        {
+            keys.add( ConfigurationProperties.PREFIX_PRIORITY + sn.substring( 0, sn.length() - FACTORY_SUFFIX.length() ) );
+        }
+        return keys.toArray( new String[keys.size()] );
+    }
+
+    public boolean isEmpty()
+    {
+        return components.isEmpty();
+    }
+
+    public List<PrioritizedComponent<T>> getAll()
+    {
+        return components;
+    }
+
+    public List<PrioritizedComponent<T>> getEnabled()
+    {
+        return components.subList( 0, firstDisabled );
+    }
+
+    public void list( StringBuilder buffer )
+    {
+        for ( int i = 0; i < components.size(); i++ )
+        {
+            if ( i > 0 )
+            {
+                buffer.append( ", " );
+            }
+            PrioritizedComponent<?> component = components.get( i );
+            buffer.append( component.getType().getSimpleName() );
+            if ( component.isDisabled() )
+            {
+                buffer.append( " (disabled)" );
+            }
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return components.toString();
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SafeTransferListener.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SafeTransferListener.java
new file mode 100644
index 0000000..1ba8a37
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SafeTransferListener.java
@@ -0,0 +1,188 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferListener;
+
+class SafeTransferListener
+    extends AbstractTransferListener
+{
+
+    private final Logger logger;
+
+    private final TransferListener listener;
+
+    public static TransferListener wrap( RepositorySystemSession session, Logger logger )
+    {
+        TransferListener listener = session.getTransferListener();
+        if ( listener == null )
+        {
+            return null;
+        }
+        return new SafeTransferListener( listener, logger );
+    }
+
+    protected SafeTransferListener( RepositorySystemSession session, Logger logger )
+    {
+        this( session.getTransferListener(), logger );
+    }
+
+    private SafeTransferListener( TransferListener listener, Logger logger )
+    {
+        this.listener = listener;
+        this.logger = logger;
+    }
+
+    private void logError( TransferEvent event, Throwable e )
+    {
+        String msg = "Failed to dispatch transfer event '" + event + "' to " + listener.getClass().getCanonicalName();
+        logger.debug( msg, e );
+    }
+
+    @Override
+    public void transferInitiated( TransferEvent event )
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferInitiated( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferStarted( TransferEvent event )
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferStarted( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferProgressed( TransferEvent event )
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferProgressed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferCorrupted( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferSucceeded( TransferEvent event )
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferSucceeded( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferFailed( TransferEvent event )
+    {
+        if ( listener != null )
+        {
+            try
+            {
+                listener.transferFailed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                logError( event, e );
+            }
+            catch ( LinkageError e )
+            {
+                logError( event, e );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java
new file mode 100644
index 0000000..9d3d234
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java
@@ -0,0 +1,99 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * A simple digester for strings.
+ */
+class SimpleDigest
+{
+
+    private MessageDigest digest;
+
+    private long hash;
+
+    public SimpleDigest()
+    {
+        try
+        {
+            digest = MessageDigest.getInstance( "SHA-1" );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            try
+            {
+                digest = MessageDigest.getInstance( "MD5" );
+            }
+            catch ( NoSuchAlgorithmException ne )
+            {
+                digest = null;
+                hash = 13;
+            }
+        }
+    }
+
+    public void update( String data )
+    {
+        if ( data == null || data.length() <= 0 )
+        {
+            return;
+        }
+        if ( digest != null )
+        {
+            digest.update( data.getBytes( StandardCharsets.UTF_8 ) );
+        }
+        else
+        {
+            hash = hash * 31 + data.hashCode();
+        }
+    }
+
+    public String digest()
+    {
+        if ( digest != null )
+        {
+            StringBuilder buffer = new StringBuilder( 64 );
+
+            byte[] bytes = digest.digest();
+            for ( byte aByte : bytes )
+            {
+                int b = aByte & 0xFF;
+
+                if ( b < 0x10 )
+                {
+                    buffer.append( '0' );
+                }
+
+                buffer.append( Integer.toHexString( b ) );
+            }
+
+            return buffer.toString();
+        }
+        else
+        {
+            return Long.toHexString( hash );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
new file mode 100644
index 0000000..7d97233
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
@@ -0,0 +1,267 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import static java.util.Objects.requireNonNull;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.log.Logger;
+
+/**
+ * A local repository manager that realizes the classical Maven 2.0 local repository.
+ */
+class SimpleLocalRepositoryManager
+    implements LocalRepositoryManager
+{
+
+    private final LocalRepository repository;
+
+    public SimpleLocalRepositoryManager( File basedir )
+    {
+        this( basedir, "simple" );
+    }
+
+    public SimpleLocalRepositoryManager( String basedir )
+    {
+        this( ( basedir != null ) ? new File( basedir ) : null, "simple" );
+    }
+
+    SimpleLocalRepositoryManager( File basedir, String type )
+    {
+        requireNonNull( basedir, "base directory cannot be null" );
+        repository = new LocalRepository( basedir.getAbsoluteFile(), type );
+    }
+
+    public SimpleLocalRepositoryManager setLogger( Logger logger )
+    {
+        return this;
+    }
+
+    public LocalRepository getRepository()
+    {
+        return repository;
+    }
+
+    String getPathForArtifact( Artifact artifact, boolean local )
+    {
+        StringBuilder path = new StringBuilder( 128 );
+
+        path.append( artifact.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+        path.append( artifact.getArtifactId() ).append( '/' );
+
+        path.append( artifact.getBaseVersion() ).append( '/' );
+
+        path.append( artifact.getArtifactId() ).append( '-' );
+        if ( local )
+        {
+            path.append( artifact.getBaseVersion() );
+        }
+        else
+        {
+            path.append( artifact.getVersion() );
+        }
+
+        if ( artifact.getClassifier().length() > 0 )
+        {
+            path.append( '-' ).append( artifact.getClassifier() );
+        }
+
+        if ( artifact.getExtension().length() > 0 )
+        {
+            path.append( '.' ).append( artifact.getExtension() );
+        }
+
+        return path.toString();
+    }
+
+    public String getPathForLocalArtifact( Artifact artifact )
+    {
+        return getPathForArtifact( artifact, true );
+    }
+
+    public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+    {
+        return getPathForArtifact( artifact, false );
+    }
+
+    public String getPathForLocalMetadata( Metadata metadata )
+    {
+        return getPath( metadata, "local" );
+    }
+
+    public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+    {
+        return getPath( metadata, getRepositoryKey( repository, context ) );
+    }
+
+    String getRepositoryKey( RemoteRepository repository, String context )
+    {
+        String key;
+
+        if ( repository.isRepositoryManager() )
+        {
+            // repository serves dynamic contents, take request parameters into account for key
+
+            StringBuilder buffer = new StringBuilder( 128 );
+
+            buffer.append( repository.getId() );
+
+            buffer.append( '-' );
+
+            SortedSet<String> subKeys = new TreeSet<String>();
+            for ( RemoteRepository mirroredRepo : repository.getMirroredRepositories() )
+            {
+                subKeys.add( mirroredRepo.getId() );
+            }
+
+            SimpleDigest digest = new SimpleDigest();
+            digest.update( context );
+            for ( String subKey : subKeys )
+            {
+                digest.update( subKey );
+            }
+            buffer.append( digest.digest() );
+
+            key = buffer.toString();
+        }
+        else
+        {
+            // repository serves static contents, its id is sufficient as key
+
+            key = repository.getId();
+        }
+
+        return key;
+    }
+
+    private String getPath( Metadata metadata, String repositoryKey )
+    {
+        StringBuilder path = new StringBuilder( 128 );
+
+        if ( metadata.getGroupId().length() > 0 )
+        {
+            path.append( metadata.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+            if ( metadata.getArtifactId().length() > 0 )
+            {
+                path.append( metadata.getArtifactId() ).append( '/' );
+
+                if ( metadata.getVersion().length() > 0 )
+                {
+                    path.append( metadata.getVersion() ).append( '/' );
+                }
+            }
+        }
+
+        path.append( insertRepositoryKey( metadata.getType(), repositoryKey ) );
+
+        return path.toString();
+    }
+
+    private String insertRepositoryKey( String filename, String repositoryKey )
+    {
+        String result;
+        int idx = filename.indexOf( '.' );
+        if ( idx < 0 )
+        {
+            result = filename + '-' + repositoryKey;
+        }
+        else
+        {
+            result = filename.substring( 0, idx ) + '-' + repositoryKey + filename.substring( idx );
+        }
+        return result;
+    }
+
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    {
+        String path = getPathForArtifact( request.getArtifact(), false );
+        File file = new File( getRepository().getBasedir(), path );
+
+        LocalArtifactResult result = new LocalArtifactResult( request );
+        if ( file.isFile() )
+        {
+            result.setFile( file );
+            result.setAvailable( true );
+        }
+
+        return result;
+    }
+
+    public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+    {
+        // noop
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getRepository() );
+    }
+
+    public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
+    {
+        LocalMetadataResult result = new LocalMetadataResult( request );
+
+        String path;
+
+        Metadata metadata = request.getMetadata();
+        String context = request.getContext();
+        RemoteRepository remote = request.getRepository();
+
+        if ( remote != null )
+        {
+            path = getPathForRemoteMetadata( metadata, remote, context );
+        }
+        else
+        {
+            path = getPathForLocalMetadata( metadata );
+        }
+
+        File file = new File( getRepository().getBasedir(), path );
+        if ( file.isFile() )
+        {
+            result.setFile( file );
+        }
+
+        return result;
+    }
+
+    public void add( RepositorySystemSession session, LocalMetadataRegistration request )
+    {
+        // noop
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
new file mode 100644
index 0000000..3c2cf6d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
@@ -0,0 +1,100 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ * Creates local repository managers for repository type {@code "simple"}.
+ */
+@Named( "simple" )
+public class SimpleLocalRepositoryManagerFactory
+    implements LocalRepositoryManagerFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private float priority;
+
+    public SimpleLocalRepositoryManagerFactory()
+    {
+        // enable no-arg constructor
+    }
+
+    @Inject
+    SimpleLocalRepositoryManagerFactory( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public LocalRepositoryManager newInstance( RepositorySystemSession session, LocalRepository repository )
+        throws NoLocalRepositoryManagerException
+    {
+        if ( "".equals( repository.getContentType() ) || "simple".equals( repository.getContentType() ) )
+        {
+            return new SimpleLocalRepositoryManager( repository.getBasedir() ).setLogger( logger );
+        }
+        else
+        {
+            throw new NoLocalRepositoryManagerException( repository );
+        }
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    public SimpleLocalRepositoryManagerFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, SimpleLocalRepositoryManager.class );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public SimpleLocalRepositoryManagerFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
new file mode 100644
index 0000000..0e4a18e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
@@ -0,0 +1,240 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.Map;
+import java.util.Properties;
+
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+
+/**
+ * Manages potentially concurrent accesses to a properties file.
+ */
+class TrackingFileManager
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    public TrackingFileManager setLogger( Logger logger )
+    {
+        this.logger = ( logger != null ) ? logger : NullLoggerFactory.LOGGER;
+        return this;
+    }
+
+    public Properties read( File file )
+    {
+        synchronized ( getLock( file ) )
+        {
+            FileLock lock = null;
+            FileInputStream stream = null;
+            try
+            {
+                if ( !file.exists() )
+                {
+                    return null;
+                }
+
+                stream = new FileInputStream( file );
+
+                lock = lock( stream.getChannel(), Math.max( 1, file.length() ), true );
+
+                Properties props = new Properties();
+                props.load( stream );
+
+                return props;
+            }
+            catch ( IOException e )
+            {
+                logger.warn( "Failed to read tracking file " + file, e );
+            }
+            finally
+            {
+                release( lock, file );
+                close( stream, file );
+            }
+        }
+
+        return null;
+    }
+
+    public Properties update( File file, Map<String, String> updates )
+    {
+        Properties props = new Properties();
+
+        synchronized ( getLock( file ) )
+        {
+            File directory = file.getParentFile();
+            if ( !directory.mkdirs() && !directory.exists() )
+            {
+                logger.warn( "Failed to create parent directories for tracking file " + file );
+                return props;
+            }
+
+            RandomAccessFile raf = null;
+            FileLock lock = null;
+            try
+            {
+                raf = new RandomAccessFile( file, "rw" );
+                lock = lock( raf.getChannel(), Math.max( 1, raf.length() ), false );
+
+                if ( file.canRead() )
+                {
+                    byte[] buffer = new byte[(int) raf.length()];
+
+                    raf.readFully( buffer );
+
+                    ByteArrayInputStream stream = new ByteArrayInputStream( buffer );
+
+                    props.load( stream );
+                }
+
+                for ( Map.Entry<String, String> update : updates.entrySet() )
+                {
+                    if ( update.getValue() == null )
+                    {
+                        props.remove( update.getKey() );
+                    }
+                    else
+                    {
+                        props.setProperty( update.getKey(), update.getValue() );
+                    }
+                }
+
+                ByteArrayOutputStream stream = new ByteArrayOutputStream( 1024 * 2 );
+
+                logger.debug( "Writing tracking file " + file );
+                props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
+                    + ", its format can be changed without prior notice." );
+
+                raf.seek( 0 );
+                raf.write( stream.toByteArray() );
+                raf.setLength( raf.getFilePointer() );
+            }
+            catch ( IOException e )
+            {
+                logger.warn( "Failed to write tracking file " + file, e );
+            }
+            finally
+            {
+                release( lock, file );
+                close( raf, file );
+            }
+        }
+
+        return props;
+    }
+
+    private void release( FileLock lock, File file )
+    {
+        if ( lock != null )
+        {
+            try
+            {
+                lock.release();
+            }
+            catch ( IOException e )
+            {
+                logger.warn( "Error releasing lock for tracking file " + file, e );
+            }
+        }
+    }
+
+    private void close( Closeable closeable, File file )
+    {
+        if ( closeable != null )
+        {
+            try
+            {
+                closeable.close();
+            }
+            catch ( IOException e )
+            {
+                logger.warn( "Error closing tracking file " + file, e );
+            }
+        }
+    }
+
+    private Object getLock( File file )
+    {
+        /*
+         * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another
+         * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
+         * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
+         */
+        try
+        {
+            return file.getCanonicalPath().intern();
+        }
+        catch ( IOException e )
+        {
+            logger.warn( "Failed to canonicalize path " + file + ": " + e.getMessage() );
+            return file.getAbsolutePath().intern();
+        }
+    }
+
+    private FileLock lock( FileChannel channel, long size, boolean shared )
+        throws IOException
+    {
+        FileLock lock = null;
+
+        for ( int attempts = 8; attempts >= 0; attempts-- )
+        {
+            try
+            {
+                lock = channel.lock( 0, size, shared );
+                break;
+            }
+            catch ( OverlappingFileLockException e )
+            {
+                if ( attempts <= 0 )
+                {
+                    throw (IOException) new IOException().initCause( e );
+                }
+                try
+                {
+                    Thread.sleep( 50L );
+                }
+                catch ( InterruptedException e1 )
+                {
+                    Thread.currentThread().interrupt();
+                }
+            }
+        }
+
+        if ( lock == null )
+        {
+            throw new IOException( "Could not lock file" );
+        }
+
+        return lock;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java
new file mode 100644
index 0000000..deb830d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/Utils.java
@@ -0,0 +1,128 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.MetadataGenerator;
+import org.eclipse.aether.impl.MetadataGeneratorFactory;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+
+/**
+ * Internal utility methods.
+ */
+final class Utils
+{
+
+    public static PrioritizedComponents<MetadataGeneratorFactory> sortMetadataGeneratorFactories( RepositorySystemSession session,
+                                                                                                  Collection<? extends MetadataGeneratorFactory> factories )
+    {
+        PrioritizedComponents<MetadataGeneratorFactory> result =
+            new PrioritizedComponents<MetadataGeneratorFactory>( session );
+        for ( MetadataGeneratorFactory factory : factories )
+        {
+            result.add( factory, factory.getPriority() );
+        }
+        return result;
+    }
+
+    public static List<Metadata> prepareMetadata( List<? extends MetadataGenerator> generators,
+                                                  List<? extends Artifact> artifacts )
+    {
+        List<Metadata> metadatas = new ArrayList<Metadata>();
+
+        for ( MetadataGenerator generator : generators )
+        {
+            metadatas.addAll( generator.prepare( artifacts ) );
+        }
+
+        return metadatas;
+    }
+
+    public static List<Metadata> finishMetadata( List<? extends MetadataGenerator> generators,
+                                                 List<? extends Artifact> artifacts )
+    {
+        List<Metadata> metadatas = new ArrayList<Metadata>();
+
+        for ( MetadataGenerator generator : generators )
+        {
+            metadatas.addAll( generator.finish( artifacts ) );
+        }
+
+        return metadatas;
+    }
+
+    public static <T> List<T> combine( Collection<? extends T> first, Collection<? extends T> second )
+    {
+        List<T> result = new ArrayList<T>( first.size() + second.size() );
+        result.addAll( first );
+        result.addAll( second );
+        return result;
+    }
+
+    public static int getPolicy( RepositorySystemSession session, Artifact artifact, RemoteRepository repository )
+    {
+        ResolutionErrorPolicy rep = session.getResolutionErrorPolicy();
+        if ( rep == null )
+        {
+            return ResolutionErrorPolicy.CACHE_DISABLED;
+        }
+        return rep.getArtifactPolicy( session, new ResolutionErrorPolicyRequest<Artifact>( artifact, repository ) );
+    }
+
+    public static int getPolicy( RepositorySystemSession session, Metadata metadata, RemoteRepository repository )
+    {
+        ResolutionErrorPolicy rep = session.getResolutionErrorPolicy();
+        if ( rep == null )
+        {
+            return ResolutionErrorPolicy.CACHE_DISABLED;
+        }
+        return rep.getMetadataPolicy( session, new ResolutionErrorPolicyRequest<Metadata>( metadata, repository ) );
+    }
+
+    public static void appendClassLoader( StringBuilder buffer, Object component )
+    {
+        ClassLoader loader = component.getClass().getClassLoader();
+        if ( loader != null && !loader.equals( Utils.class.getClassLoader() ) )
+        {
+            buffer.append( " from " ).append( loader );
+        }
+    }
+
+    public static void checkOffline( RepositorySystemSession session, OfflineController offlineController,
+                                     RemoteRepository repository )
+        throws RepositoryOfflineException
+    {
+        if ( session.isOffline() )
+        {
+            offlineController.checkOffline( session, repository );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java
new file mode 100644
index 0000000..b5e72ec
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.TransferResource;
+
+/**
+ * Implements {@link org.eclipse.aether.repository.RepositoryPolicy#CHECKSUM_POLICY_WARN}.
+ */
+final class WarnChecksumPolicy
+    extends AbstractChecksumPolicy
+{
+
+    public WarnChecksumPolicy( LoggerFactory loggerFactory, TransferResource resource )
+    {
+        super( loggerFactory, resource );
+    }
+
+    public boolean onTransferChecksumFailure( ChecksumFailureException exception )
+    {
+        String msg =
+            "Could not validate integrity of download from " + resource.getRepositoryUrl() + resource.getResourceName();
+        if ( logger.isDebugEnabled() )
+        {
+            logger.warn( msg, exception );
+        }
+        else
+        {
+            logger.warn( msg + ": " + exception.getMessage() );
+        }
+        return true;
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/package-info.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/package-info.java
new file mode 100644
index 0000000..813b21d
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The various sub components that collectively implement the repository system. 
+ */
+package org.eclipse.aether.internal.impl;
+
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/Slf4jLoggerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/Slf4jLoggerFactory.java
new file mode 100644
index 0000000..840fe21
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/Slf4jLoggerFactory.java
@@ -0,0 +1,201 @@
+package org.eclipse.aether.internal.impl.slf4j;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.sisu.Nullable;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.spi.LocationAwareLogger;
+
+/**
+ * A logger factory that delegates to <a href="http://www.slf4j.org/" target="_blank">SLF4J</a> logging.
+ */
+@Named( "slf4j" )
+public class Slf4jLoggerFactory
+    implements LoggerFactory, Service
+{
+
+    private static final boolean AVAILABLE;
+
+    static
+    {
+        boolean available;
+        try
+        {
+            Slf4jLoggerFactory.class.getClassLoader().loadClass( "org.slf4j.ILoggerFactory" );
+            available = true;
+        }
+        catch ( Exception e )
+        {
+            available = false;
+        }
+        catch ( LinkageError e )
+        {
+            available = false;
+        }
+        AVAILABLE = available;
+    }
+
+    public static boolean isSlf4jAvailable()
+    {
+        return AVAILABLE;
+    }
+
+    private ILoggerFactory factory;
+
+    /**
+     * Creates an instance of this logger factory.
+     */
+    public Slf4jLoggerFactory()
+    {
+        // enables no-arg constructor
+    }
+
+    @Inject
+    Slf4jLoggerFactory( @Nullable ILoggerFactory factory )
+    {
+        setLoggerFactory( factory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( ILoggerFactory.class ) );
+    }
+
+    public Slf4jLoggerFactory setLoggerFactory( ILoggerFactory factory )
+    {
+        this.factory = factory;
+        return this;
+    }
+
+    public Logger getLogger( String name )
+    {
+        org.slf4j.Logger logger = getFactory().getLogger( name );
+        if ( logger instanceof LocationAwareLogger )
+        {
+            return new Slf4jLoggerEx( (LocationAwareLogger) logger );
+        }
+        return new Slf4jLogger( logger );
+    }
+
+    private ILoggerFactory getFactory()
+    {
+        if ( factory == null )
+        {
+            factory = org.slf4j.LoggerFactory.getILoggerFactory();
+        }
+        return factory;
+    }
+
+    private static final class Slf4jLogger
+        implements Logger
+    {
+
+        private final org.slf4j.Logger logger;
+
+        public Slf4jLogger( org.slf4j.Logger logger )
+        {
+            this.logger = logger;
+        }
+
+        public boolean isDebugEnabled()
+        {
+            return logger.isDebugEnabled();
+        }
+
+        public void debug( String msg )
+        {
+            logger.debug( msg );
+        }
+
+        public void debug( String msg, Throwable error )
+        {
+            logger.debug( msg, error );
+        }
+
+        public boolean isWarnEnabled()
+        {
+            return logger.isWarnEnabled();
+        }
+
+        public void warn( String msg )
+        {
+            logger.warn( msg );
+        }
+
+        public void warn( String msg, Throwable error )
+        {
+            logger.warn( msg, error );
+        }
+
+    }
+
+    private static final class Slf4jLoggerEx
+        implements Logger
+    {
+
+        private static final String FQCN = Slf4jLoggerEx.class.getName();
+
+        private final LocationAwareLogger logger;
+
+        public Slf4jLoggerEx( LocationAwareLogger logger )
+        {
+            this.logger = logger;
+        }
+
+        public boolean isDebugEnabled()
+        {
+            return logger.isDebugEnabled();
+        }
+
+        public void debug( String msg )
+        {
+            logger.log( null, FQCN, LocationAwareLogger.DEBUG_INT, msg, null, null );
+        }
+
+        public void debug( String msg, Throwable error )
+        {
+            logger.log( null, FQCN, LocationAwareLogger.DEBUG_INT, msg, null, error );
+        }
+
+        public boolean isWarnEnabled()
+        {
+            return logger.isWarnEnabled();
+        }
+
+        public void warn( String msg )
+        {
+            logger.log( null, FQCN, LocationAwareLogger.WARN_INT, msg, null, null );
+        }
+
+        public void warn( String msg, Throwable error )
+        {
+            logger.log( null, FQCN, LocationAwareLogger.WARN_INT, msg, null, error );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/package-info.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/package-info.java
new file mode 100644
index 0000000..307c22e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/slf4j/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The integration with the logging framework <a href="http://www.slf4j.org/" target="_blank">SLF4J</a>. 
+ */
+package org.eclipse.aether.internal.impl.slf4j;
+
diff --git a/maven-resolver-impl/src/site/site.xml b/maven-resolver-impl/src/site/site.xml
new file mode 100644
index 0000000..946c950
--- /dev/null
+++ b/maven-resolver-impl/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Implementation">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/DefaultServiceLocatorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/DefaultServiceLocatorTest.java
new file mode 100644
index 0000000..657b4ac
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/DefaultServiceLocatorTest.java
@@ -0,0 +1,103 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultServiceLocatorTest
+{
+
+    @Test
+    public void testGetRepositorySystem()
+    {
+        DefaultServiceLocator locator = new DefaultServiceLocator();
+        locator.addService( ArtifactDescriptorReader.class, StubArtifactDescriptorReader.class );
+        locator.addService( VersionResolver.class, StubVersionResolver.class );
+        locator.addService( VersionRangeResolver.class, StubVersionRangeResolver.class );
+
+        RepositorySystem repoSys = locator.getService( RepositorySystem.class );
+        assertNotNull( repoSys );
+    }
+
+    @Test
+    public void testGetServicesUnmodifiable()
+    {
+        DefaultServiceLocator locator = new DefaultServiceLocator();
+        locator.setServices( String.class, "one", "two" );
+        List<String> services = locator.getServices( String.class );
+        assertNotNull( services );
+        try
+        {
+            services.set( 0, "fail" );
+            fail( "service list is modifable" );
+        }
+        catch ( UnsupportedOperationException e )
+        {
+            // expected
+        }
+    }
+
+    @Test
+    public void testSetInstancesAddClass()
+    {
+        DefaultServiceLocator locator = new DefaultServiceLocator();
+        locator.setServices( String.class, "one", "two" );
+        locator.addService( String.class, String.class );
+        assertEquals( Arrays.asList( "one", "two", "" ), locator.getServices( String.class ) );
+    }
+
+    @Test
+    public void testInitService()
+    {
+        DefaultServiceLocator locator = new DefaultServiceLocator();
+        locator.setService( DummyService.class, DummyService.class );
+        DummyService service = locator.getService( DummyService.class );
+        assertNotNull( service );
+        assertNotNull( service.locator );
+    }
+
+    private static class DummyService
+        implements Service
+    {
+
+        public ServiceLocator locator;
+
+        public void initService( ServiceLocator locator )
+        {
+            this.locator = locator;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubArtifactDescriptorReader.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubArtifactDescriptorReader.java
new file mode 100644
index 0000000..a5e650f
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubArtifactDescriptorReader.java
@@ -0,0 +1,40 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+
+public class StubArtifactDescriptorReader
+    implements ArtifactDescriptorReader
+{
+
+    public ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session,
+                                                            ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException
+    {
+        ArtifactDescriptorResult result = new ArtifactDescriptorResult( request );
+        return result;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionRangeResolver.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionRangeResolver.java
new file mode 100644
index 0000000..81e000e
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionRangeResolver.java
@@ -0,0 +1,39 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+
+public class StubVersionRangeResolver
+    implements VersionRangeResolver
+{
+
+    public VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException
+    {
+        VersionRangeResult result = new VersionRangeResult( request );
+        return result;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionResolver.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionResolver.java
new file mode 100644
index 0000000..f59fa11
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/StubVersionResolver.java
@@ -0,0 +1,39 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+
+public class StubVersionResolver
+    implements VersionResolver
+{
+
+    public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException
+    {
+        VersionResult result = new VersionResult( request );
+        return result;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/guice/AetherModuleTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/guice/AetherModuleTest.java
new file mode 100644
index 0000000..efcda19
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/impl/guice/AetherModuleTest.java
@@ -0,0 +1,85 @@
+package org.eclipse.aether.impl.guice;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.MetadataGeneratorFactory;
+import org.eclipse.aether.impl.StubArtifactDescriptorReader;
+import org.eclipse.aether.impl.StubVersionRangeResolver;
+import org.eclipse.aether.impl.StubVersionResolver;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Provides;
+
+public class AetherModuleTest
+{
+
+    @Test
+    public void testModuleCompleteness()
+    {
+        assertNotNull( Guice.createInjector( new SystemModule() ).getInstance( RepositorySystem.class ) );
+    }
+
+    static class SystemModule
+        extends AbstractModule
+    {
+
+        @Override
+        protected void configure()
+        {
+            install( new AetherModule() );
+            bind( ArtifactDescriptorReader.class ).to( StubArtifactDescriptorReader.class );
+            bind( VersionRangeResolver.class ).to( StubVersionRangeResolver.class );
+            bind( VersionResolver.class ).to( StubVersionResolver.class );
+        }
+
+        @Provides
+        public Set<MetadataGeneratorFactory> metadataGeneratorFactories()
+        {
+            return Collections.emptySet();
+        }
+
+        @Provides
+        public Set<RepositoryConnectorFactory> repositoryConnectorFactories()
+        {
+            return Collections.emptySet();
+        }
+
+        @Provides
+        public Set<TransporterFactory> transporterFactories()
+        {
+            return Collections.emptySet();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DataPoolTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DataPoolTest.java
new file mode 100644
index 0000000..43651f6
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DataPoolTest.java
@@ -0,0 +1,66 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.junit.Test;
+
+public class DataPoolTest
+{
+
+    private DataPool newDataPool()
+    {
+        return new DataPool( new DefaultRepositorySystemSession() );
+    }
+
+    @Test
+    public void testArtifactDescriptorCaching()
+    {
+        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest();
+        request.setArtifact( new DefaultArtifact( "gid:aid:1" ) );
+        ArtifactDescriptorResult result = new ArtifactDescriptorResult( request );
+        result.setArtifact( new DefaultArtifact( "gid:aid:2" ) );
+        result.addRelocation( request.getArtifact() );
+        result.addDependency( new Dependency( new DefaultArtifact( "gid:dep:3" ), "compile" ) );
+        result.addManagedDependency( new Dependency( new DefaultArtifact( "gid:mdep:3" ), "runtime" ) );
+        result.addRepository( new RemoteRepository.Builder( "test", "default", "http://localhost" ).build() );
+        result.addAlias( new DefaultArtifact( "gid:alias:4" ) );
+
+        DataPool pool = newDataPool();
+        Object key = pool.toKey( request );
+        pool.putDescriptor( key, result );
+        ArtifactDescriptorResult cached = pool.getDescriptor( key, request );
+        assertNotNull( cached );
+        assertEquals( result.getArtifact(), cached.getArtifact() );
+        assertEquals( result.getRelocations(), cached.getRelocations() );
+        assertEquals( result.getDependencies(), cached.getDependencies() );
+        assertEquals( result.getManagedDependencies(), cached.getManagedDependencies() );
+        assertEquals( result.getRepositories(), cached.getRepositories() );
+        assertEquals( result.getAliases(), cached.getAliases() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
new file mode 100644
index 0000000..f776e9c
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
@@ -0,0 +1,909 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
+import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
+import org.eclipse.aether.internal.test.util.TestFileProcessor;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.repository.WorkspaceRepository;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.util.repository.SimpleResolutionErrorPolicy;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultArtifactResolverTest
+{
+    private DefaultArtifactResolver resolver;
+
+    private DefaultRepositorySystemSession session;
+
+    private TestLocalRepositoryManager lrm;
+
+    private StubRepositoryConnectorProvider repositoryConnectorProvider;
+
+    private Artifact artifact;
+
+    private RecordingRepositoryConnector connector;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        UpdateCheckManager updateCheckManager = new StaticUpdateCheckManager( true );
+        repositoryConnectorProvider = new StubRepositoryConnectorProvider();
+        VersionResolver versionResolver = new StubVersionResolver();
+        session = TestUtils.newSession();
+        lrm = (TestLocalRepositoryManager) session.getLocalRepositoryManager();
+        resolver = new DefaultArtifactResolver();
+        resolver.setFileProcessor( new TestFileProcessor() );
+        resolver.setRepositoryEventDispatcher( new StubRepositoryEventDispatcher() );
+        resolver.setVersionResolver( versionResolver );
+        resolver.setUpdateCheckManager( updateCheckManager );
+        resolver.setRepositoryConnectorProvider( repositoryConnectorProvider );
+        resolver.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
+        resolver.setSyncContextFactory( new StubSyncContextFactory() );
+        resolver.setOfflineController( new DefaultOfflineController() );
+
+        artifact = new DefaultArtifact( "gid", "aid", "", "ext", "ver" );
+
+        connector = new RecordingRepositoryConnector();
+        repositoryConnectorProvider.setConnector( connector );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        if ( session.getLocalRepository() != null )
+        {
+            TestFileUtils.deleteFile( session.getLocalRepository().getBasedir() );
+        }
+    }
+
+    @Test
+    public void testResolveLocalArtifactSuccessful()
+        throws IOException, ArtifactResolutionException
+    {
+        File tmpFile = TestFileUtils.createTempFile( "tmp" );
+        Map<String, String> properties = new HashMap<String, String>();
+        properties.put( ArtifactProperties.LOCAL_PATH, tmpFile.getAbsolutePath() );
+        artifact = artifact.setProperties( properties );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+        resolved = resolved.setFile( null );
+
+        assertEquals( artifact, resolved );
+    }
+
+    @Test
+    public void testResolveLocalArtifactUnsuccessful()
+        throws IOException, ArtifactResolutionException
+    {
+        File tmpFile = TestFileUtils.createTempFile( "tmp" );
+        Map<String, String> properties = new HashMap<String, String>();
+        properties.put( ArtifactProperties.LOCAL_PATH, tmpFile.getAbsolutePath() );
+        artifact = artifact.setProperties( properties );
+
+        tmpFile.delete();
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            assertNotNull( e.getResults() );
+            assertEquals( 1, e.getResults().size() );
+
+            ArtifactResult result = e.getResults().get( 0 );
+
+            assertSame( request, result.getRequest() );
+
+            assertFalse( result.getExceptions().isEmpty() );
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactNotFoundException );
+
+            Artifact resolved = result.getArtifact();
+            assertNull( resolved );
+        }
+
+    }
+
+    @Test
+    public void testResolveRemoteArtifact()
+        throws IOException, ArtifactResolutionException
+    {
+        connector.setExpectGet( artifact );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test
+    public void testResolveRemoteArtifactUnsuccessful()
+        throws IOException, ArtifactResolutionException
+    {
+        RecordingRepositoryConnector connector = new RecordingRepositoryConnector()
+        {
+
+            @Override
+            public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                             Collection<? extends MetadataDownload> metadataDownloads )
+            {
+                super.get( artifactDownloads, metadataDownloads );
+                ArtifactDownload download = artifactDownloads.iterator().next();
+                ArtifactTransferException exception =
+                    new ArtifactNotFoundException( download.getArtifact(), null, "not found" );
+                download.setException( exception );
+            }
+
+        };
+
+        connector.setExpectGet( artifact );
+        repositoryConnectorProvider.setConnector( connector );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            connector.assertSeenExpected();
+            assertNotNull( e.getResults() );
+            assertEquals( 1, e.getResults().size() );
+
+            ArtifactResult result = e.getResults().get( 0 );
+
+            assertSame( request, result.getRequest() );
+
+            assertFalse( result.getExceptions().isEmpty() );
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactNotFoundException );
+
+            Artifact resolved = result.getArtifact();
+            assertNull( resolved );
+        }
+
+    }
+
+    @Test
+    public void testArtifactNotFoundCache()
+        throws Exception
+    {
+        RecordingRepositoryConnector connector = new RecordingRepositoryConnector()
+        {
+            @Override
+            public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                             Collection<? extends MetadataDownload> metadataDownloads )
+            {
+                super.get( artifactDownloads, metadataDownloads );
+                for ( ArtifactDownload download : artifactDownloads )
+                {
+                    download.getFile().delete();
+                    ArtifactTransferException exception =
+                        new ArtifactNotFoundException( download.getArtifact(), null, "not found" );
+                    download.setException( exception );
+                }
+            }
+        };
+
+        repositoryConnectorProvider.setConnector( connector );
+        resolver.setUpdateCheckManager( new DefaultUpdateCheckManager().setUpdatePolicyAnalyzer( new DefaultUpdatePolicyAnalyzer() ) );
+
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+        session.setUpdatePolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+
+        RemoteRepository remoteRepo = new RemoteRepository.Builder( "id", "default", "file:///" ).build();
+
+        Artifact artifact1 = artifact;
+        Artifact artifact2 = artifact.setVersion( "ver2" );
+
+        ArtifactRequest request1 = new ArtifactRequest( artifact1, Arrays.asList( remoteRepo ), "" );
+        ArtifactRequest request2 = new ArtifactRequest( artifact2, Arrays.asList( remoteRepo ), "" );
+
+        connector.setExpectGet( new Artifact[] { artifact1, artifact2 } );
+        try
+        {
+            resolver.resolveArtifacts( session, Arrays.asList( request1, request2 ) );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            connector.assertSeenExpected();
+        }
+
+        TestFileUtils.writeString( new File( lrm.getRepository().getBasedir(), lrm.getPathForLocalArtifact( artifact2 ) ),
+                             "artifact" );
+        lrm.setArtifactAvailability( artifact2, false );
+
+        DefaultUpdateCheckManagerTest.resetSessionData( session );
+        connector.resetActual();
+        connector.setExpectGet( new Artifact[0] );
+        try
+        {
+            resolver.resolveArtifacts( session, Arrays.asList( request1, request2 ) );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            connector.assertSeenExpected();
+            for ( ArtifactResult result : e.getResults() )
+            {
+                Throwable t = result.getExceptions().get( 0 );
+                assertEquals( t.toString(), true, t instanceof ArtifactNotFoundException );
+                assertEquals( t.toString(), true, t.getMessage().contains( "cached" ) );
+            }
+        }
+    }
+
+    @Test
+    public void testResolveFromWorkspace()
+        throws IOException, ArtifactResolutionException
+    {
+        WorkspaceReader workspace = new WorkspaceReader()
+        {
+
+            public WorkspaceRepository getRepository()
+            {
+                return new WorkspaceRepository( "default" );
+            }
+
+            public List<String> findVersions( Artifact artifact )
+            {
+                return Arrays.asList( artifact.getVersion() );
+            }
+
+            public File findArtifact( Artifact artifact )
+            {
+                try
+                {
+                    return TestFileUtils.createTempFile( artifact.toString() );
+                }
+                catch ( IOException e )
+                {
+                    throw new RuntimeException( e.getMessage(), e );
+                }
+            }
+        };
+        session.setWorkspaceReader( workspace );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        assertEquals( resolved.toString(), TestFileUtils.readString( resolved.getFile() ) );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test
+    public void testResolveFromWorkspaceFallbackToRepository()
+        throws IOException, ArtifactResolutionException
+    {
+        WorkspaceReader workspace = new WorkspaceReader()
+        {
+
+            public WorkspaceRepository getRepository()
+            {
+                return new WorkspaceRepository( "default" );
+            }
+
+            public List<String> findVersions( Artifact artifact )
+            {
+                return Arrays.asList( artifact.getVersion() );
+            }
+
+            public File findArtifact( Artifact artifact )
+            {
+                return null;
+            }
+        };
+        session.setWorkspaceReader( workspace );
+
+        connector.setExpectGet( artifact );
+        repositoryConnectorProvider.setConnector( connector );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( "exception on resolveArtifact", result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test
+    public void testRepositoryEventsSuccessfulLocal()
+        throws ArtifactResolutionException, IOException
+    {
+        RecordingRepositoryListener listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        File tmpFile = TestFileUtils.createTempFile( "tmp" );
+        Map<String, String> properties = new HashMap<String, String>();
+        properties.put( ArtifactProperties.LOCAL_PATH, tmpFile.getAbsolutePath() );
+        artifact = artifact.setProperties( properties );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        resolver.resolveArtifact( session, request );
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( 2, events.size() );
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( EventType.ARTIFACT_RESOLVING, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact() );
+
+        event = events.get( 1 );
+        assertEquals( EventType.ARTIFACT_RESOLVED, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact().setFile( null ) );
+    }
+
+    @Test
+    public void testRepositoryEventsUnsuccessfulLocal()
+        throws IOException
+    {
+        RecordingRepositoryListener listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        Map<String, String> properties = new HashMap<String, String>();
+        properties.put( ArtifactProperties.LOCAL_PATH, "doesnotexist" );
+        artifact = artifact.setProperties( properties );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+        }
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( 2, events.size() );
+
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVING, event.getType() );
+
+        event = events.get( 1 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVED, event.getType() );
+        assertNotNull( event.getException() );
+        assertEquals( 1, event.getExceptions().size() );
+
+    }
+
+    @Test
+    public void testRepositoryEventsSuccessfulRemote()
+        throws ArtifactResolutionException
+    {
+        RecordingRepositoryListener listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        resolver.resolveArtifact( session, request );
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( events.toString(), 4, events.size() );
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( EventType.ARTIFACT_RESOLVING, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact() );
+
+        event = events.get( 1 );
+        assertEquals( EventType.ARTIFACT_DOWNLOADING, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact().setFile( null ) );
+
+        event = events.get( 2 );
+        assertEquals( EventType.ARTIFACT_DOWNLOADED, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact().setFile( null ) );
+
+        event = events.get( 3 );
+        assertEquals( EventType.ARTIFACT_RESOLVED, event.getType() );
+        assertNull( event.getException() );
+        assertEquals( artifact, event.getArtifact().setFile( null ) );
+    }
+
+    @Test
+    public void testRepositoryEventsUnsuccessfulRemote()
+        throws IOException, ArtifactResolutionException
+    {
+        RecordingRepositoryConnector connector = new RecordingRepositoryConnector()
+        {
+
+            @Override
+            public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                             Collection<? extends MetadataDownload> metadataDownloads )
+            {
+                super.get( artifactDownloads, metadataDownloads );
+                ArtifactDownload download = artifactDownloads.iterator().next();
+                ArtifactTransferException exception =
+                    new ArtifactNotFoundException( download.getArtifact(), null, "not found" );
+                download.setException( exception );
+            }
+
+        };
+        repositoryConnectorProvider.setConnector( connector );
+
+        RecordingRepositoryListener listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+        }
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( events.toString(), 4, events.size() );
+
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVING, event.getType() );
+
+        event = events.get( 1 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_DOWNLOADING, event.getType() );
+
+        event = events.get( 2 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_DOWNLOADED, event.getType() );
+        assertNotNull( event.getException() );
+        assertEquals( 1, event.getExceptions().size() );
+
+        event = events.get( 3 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVED, event.getType() );
+        assertNotNull( event.getException() );
+        assertEquals( 1, event.getExceptions().size() );
+    }
+
+    @Test
+    public void testVersionResolverFails()
+    {
+        resolver.setVersionResolver( new VersionResolver()
+        {
+
+            public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+                throws VersionResolutionException
+            {
+                throw new VersionResolutionException( new VersionResult( request ) );
+            }
+        } );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+            connector.assertSeenExpected();
+            assertNotNull( e.getResults() );
+            assertEquals( 1, e.getResults().size() );
+
+            ArtifactResult result = e.getResults().get( 0 );
+
+            assertSame( request, result.getRequest() );
+
+            assertFalse( result.getExceptions().isEmpty() );
+            assertTrue( result.getExceptions().get( 0 ) instanceof VersionResolutionException );
+
+            Artifact resolved = result.getArtifact();
+            assertNull( resolved );
+        }
+    }
+
+    @Test
+    public void testRepositoryEventsOnVersionResolverFail()
+    {
+        resolver.setVersionResolver( new VersionResolver()
+        {
+
+            public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+                throws VersionResolutionException
+            {
+                throw new VersionResolutionException( new VersionResult( request ) );
+            }
+        } );
+
+        RecordingRepositoryListener listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        try
+        {
+            resolver.resolveArtifact( session, request );
+            fail( "expected exception" );
+        }
+        catch ( ArtifactResolutionException e )
+        {
+        }
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( 2, events.size() );
+
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVING, event.getType() );
+
+        event = events.get( 1 );
+        assertEquals( artifact, event.getArtifact() );
+        assertEquals( EventType.ARTIFACT_RESOLVED, event.getType() );
+        assertNotNull( event.getException() );
+        assertEquals( 1, event.getExceptions().size() );
+    }
+
+    @Test
+    public void testLocalArtifactAvailable()
+        throws ArtifactResolutionException
+    {
+        session.setLocalRepositoryManager( new LocalRepositoryManager()
+        {
+
+            public LocalRepository getRepository()
+            {
+                return null;
+            }
+
+            public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForLocalMetadata( Metadata metadata )
+            {
+                return null;
+            }
+
+            public String getPathForLocalArtifact( Artifact artifact )
+            {
+                return null;
+            }
+
+            public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+            {
+
+                LocalArtifactResult result = new LocalArtifactResult( request );
+                result.setAvailable( true );
+                try
+                {
+                    result.setFile( TestFileUtils.createTempFile( "" ) );
+                }
+                catch ( IOException e )
+                {
+                    e.printStackTrace();
+                }
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+            {
+            }
+
+            public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
+            {
+                LocalMetadataResult result = new LocalMetadataResult( request );
+                try
+                {
+                    result.setFile( TestFileUtils.createTempFile( "" ) );
+                }
+                catch ( IOException e )
+                {
+                    e.printStackTrace();
+                }
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalMetadataRegistration request )
+            {
+            }
+        } );
+
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+
+    }
+
+    @Test
+    public void testFindInLocalRepositoryWhenVersionWasFoundInLocalRepository()
+        throws ArtifactResolutionException
+    {
+        session.setLocalRepositoryManager( new LocalRepositoryManager()
+        {
+
+            public LocalRepository getRepository()
+            {
+                return null;
+            }
+
+            public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForLocalMetadata( Metadata metadata )
+            {
+                return null;
+            }
+
+            public String getPathForLocalArtifact( Artifact artifact )
+            {
+                return null;
+            }
+
+            public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+            {
+
+                LocalArtifactResult result = new LocalArtifactResult( request );
+                result.setAvailable( false );
+                try
+                {
+                    result.setFile( TestFileUtils.createTempFile( "" ) );
+                }
+                catch ( IOException e )
+                {
+                    e.printStackTrace();
+                }
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+            {
+            }
+
+            public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
+            {
+                LocalMetadataResult result = new LocalMetadataResult( request );
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalMetadataRegistration request )
+            {
+            }
+        } );
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+        request.addRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+
+        resolver.setVersionResolver( new VersionResolver()
+        {
+
+            public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+                throws VersionResolutionException
+            {
+                return new VersionResult( request ).setRepository( new LocalRepository( "id" ) ).setVersion( request.getArtifact().getVersion() );
+            }
+        } );
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+    }
+
+    @Test
+    public void testFindInLocalRepositoryWhenVersionRangeWasResolvedFromLocalRepository()
+        throws ArtifactResolutionException
+    {
+        session.setLocalRepositoryManager( new LocalRepositoryManager()
+        {
+
+            public LocalRepository getRepository()
+            {
+                return null;
+            }
+
+            public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+            {
+                return null;
+            }
+
+            public String getPathForLocalMetadata( Metadata metadata )
+            {
+                return null;
+            }
+
+            public String getPathForLocalArtifact( Artifact artifact )
+            {
+                return null;
+            }
+
+            public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+            {
+
+                LocalArtifactResult result = new LocalArtifactResult( request );
+                result.setAvailable( false );
+                try
+                {
+                    result.setFile( TestFileUtils.createTempFile( "" ) );
+                }
+                catch ( IOException e )
+                {
+                    e.printStackTrace();
+                }
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+            {
+            }
+
+            public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
+            {
+                LocalMetadataResult result = new LocalMetadataResult( request );
+                return result;
+            }
+
+            public void add( RepositorySystemSession session, LocalMetadataRegistration request )
+            {
+            }
+
+        } );
+        ArtifactRequest request = new ArtifactRequest( artifact, null, "" );
+
+        resolver.setVersionResolver( new VersionResolver()
+        {
+
+            public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+                throws VersionResolutionException
+            {
+                return new VersionResult( request ).setVersion( request.getArtifact().getVersion() );
+            }
+        } );
+        ArtifactResult result = resolver.resolveArtifact( session, request );
+
+        assertTrue( result.getExceptions().isEmpty() );
+
+        Artifact resolved = result.getArtifact();
+        assertNotNull( resolved.getFile() );
+
+        resolved = resolved.setFile( null );
+        assertEquals( artifact, resolved );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java
new file mode 100644
index 0000000..542e3ea
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java
@@ -0,0 +1,145 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.transfer.TransferResource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultChecksumPolicyProviderTest
+{
+
+    private static final String CHECKSUM_POLICY_UNKNOWN = "unknown";
+
+    private DefaultRepositorySystemSession session;
+
+    private DefaultChecksumPolicyProvider provider;
+
+    private RemoteRepository repository;
+
+    private TransferResource resource;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        provider = new DefaultChecksumPolicyProvider();
+        repository = new RemoteRepository.Builder( "test", "default", "file:/void" ).build();
+        resource = new TransferResource( repository.getId(), repository.getUrl(), "file.txt", null, null );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        provider = null;
+        session = null;
+        repository = null;
+        resource = null;
+    }
+
+    @Test
+    public void testNewChecksumPolicy_Fail()
+    {
+        ChecksumPolicy policy =
+            provider.newChecksumPolicy( session, repository, resource, RepositoryPolicy.CHECKSUM_POLICY_FAIL );
+        assertNotNull( policy );
+        assertEquals( FailChecksumPolicy.class, policy.getClass() );
+    }
+
+    @Test
+    public void testNewChecksumPolicy_Warn()
+    {
+        ChecksumPolicy policy =
+            provider.newChecksumPolicy( session, repository, resource, RepositoryPolicy.CHECKSUM_POLICY_WARN );
+        assertNotNull( policy );
+        assertEquals( WarnChecksumPolicy.class, policy.getClass() );
+    }
+
+    @Test
+    public void testNewChecksumPolicy_Ignore()
+    {
+        ChecksumPolicy policy =
+            provider.newChecksumPolicy( session, repository, resource, RepositoryPolicy.CHECKSUM_POLICY_IGNORE );
+        assertNull( policy );
+    }
+
+    @Test
+    public void testNewChecksumPolicy_Unknown()
+    {
+        ChecksumPolicy policy = provider.newChecksumPolicy( session, repository, resource, CHECKSUM_POLICY_UNKNOWN );
+        assertNotNull( policy );
+        assertEquals( WarnChecksumPolicy.class, policy.getClass() );
+    }
+
+    @Test
+    public void testGetEffectiveChecksumPolicy_EqualPolicies()
+    {
+        String[] policies =
+            { RepositoryPolicy.CHECKSUM_POLICY_FAIL, RepositoryPolicy.CHECKSUM_POLICY_WARN,
+                RepositoryPolicy.CHECKSUM_POLICY_IGNORE, CHECKSUM_POLICY_UNKNOWN };
+        for ( String policy : policies )
+        {
+            assertEquals( policy, policy, provider.getEffectiveChecksumPolicy( session, policy, policy ) );
+        }
+    }
+
+    @Test
+    public void testGetEffectiveChecksumPolicy_DifferentPolicies()
+    {
+        String[][] testCases =
+            { { RepositoryPolicy.CHECKSUM_POLICY_WARN, RepositoryPolicy.CHECKSUM_POLICY_FAIL },
+                { RepositoryPolicy.CHECKSUM_POLICY_IGNORE, RepositoryPolicy.CHECKSUM_POLICY_FAIL },
+                { RepositoryPolicy.CHECKSUM_POLICY_IGNORE, RepositoryPolicy.CHECKSUM_POLICY_WARN } };
+        for ( String[] testCase : testCases )
+        {
+            assertEquals( testCase[0] + " vs " + testCase[1], testCase[0],
+                          provider.getEffectiveChecksumPolicy( session, testCase[0], testCase[1] ) );
+            assertEquals( testCase[0] + " vs " + testCase[1], testCase[0],
+                          provider.getEffectiveChecksumPolicy( session, testCase[1], testCase[0] ) );
+        }
+    }
+
+    @Test
+    public void testGetEffectiveChecksumPolicy_UnknownPolicies()
+    {
+        String[][] testCases =
+            { { RepositoryPolicy.CHECKSUM_POLICY_WARN, RepositoryPolicy.CHECKSUM_POLICY_FAIL },
+                { RepositoryPolicy.CHECKSUM_POLICY_WARN, RepositoryPolicy.CHECKSUM_POLICY_WARN },
+                { RepositoryPolicy.CHECKSUM_POLICY_IGNORE, RepositoryPolicy.CHECKSUM_POLICY_IGNORE } };
+        for ( String[] testCase : testCases )
+        {
+            assertEquals( "unknown vs " + testCase[1], testCase[0],
+                          provider.getEffectiveChecksumPolicy( session, CHECKSUM_POLICY_UNKNOWN, testCase[1] ) );
+            assertEquals( "unknown vs " + testCase[1], testCase[0],
+                          provider.getEffectiveChecksumPolicy( session, testCase[1], CHECKSUM_POLICY_UNKNOWN ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java
new file mode 100644
index 0000000..72c4602
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDependencyCollectorTest.java
@@ -0,0 +1,569 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyCycle;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+import org.eclipse.aether.util.graph.manager.ClassicDependencyManager;
+import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
+import org.eclipse.aether.util.graph.version.HighestVersionFilter;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultDependencyCollectorTest
+{
+
+    private DefaultDependencyCollector collector;
+
+    private DefaultRepositorySystemSession session;
+
+    private DependencyGraphParser parser;
+
+    private RemoteRepository repository;
+
+    private IniArtifactDescriptorReader newReader( String prefix )
+    {
+        return new IniArtifactDescriptorReader( "artifact-descriptions/" + prefix );
+    }
+
+    private Dependency newDep( String coords )
+    {
+        return newDep( coords, "" );
+    }
+
+    private Dependency newDep( String coords, String scope )
+    {
+        return new Dependency( new DefaultArtifact( coords ), scope );
+    }
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        session = TestUtils.newSession();
+
+        collector = new DefaultDependencyCollector();
+        collector.setArtifactDescriptorReader( newReader( "" ) );
+        collector.setVersionRangeResolver( new StubVersionRangeResolver() );
+        collector.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
+        collector.setLoggerFactory( new TestLoggerFactory() );
+
+        parser = new DependencyGraphParser( "artifact-descriptions/" );
+
+        repository = new RemoteRepository.Builder( "id", "default", "file:///" ).build();
+    }
+
+    private static void assertEqualSubtree( DependencyNode expected, DependencyNode actual )
+    {
+        assertEqualSubtree( expected, actual, new LinkedList<DependencyNode>() );
+    }
+
+    private static void assertEqualSubtree( DependencyNode expected, DependencyNode actual,
+                                            LinkedList<DependencyNode> parents )
+    {
+        assertEquals( "path: " + parents, expected.getDependency(), actual.getDependency() );
+
+        if ( actual.getDependency() != null )
+        {
+            Artifact artifact = actual.getDependency().getArtifact();
+            for ( DependencyNode parent : parents )
+            {
+                if ( parent.getDependency() != null && artifact.equals( parent.getDependency().getArtifact() ) )
+                {
+                    return;
+                }
+            }
+        }
+
+        parents.addLast( expected );
+
+        assertEquals( "path: " + parents + ", expected: " + expected.getChildren() + ", actual: "
+                          + actual.getChildren(), expected.getChildren().size(), actual.getChildren().size() );
+
+        Iterator<DependencyNode> iterator1 = expected.getChildren().iterator();
+        Iterator<DependencyNode> iterator2 = actual.getChildren().iterator();
+
+        while ( iterator1.hasNext() )
+        {
+            assertEqualSubtree( iterator1.next(), iterator2.next(), parents );
+        }
+
+        parents.removeLast();
+    }
+
+    private Dependency dep( DependencyNode root, int... coords )
+    {
+        return path( root, coords ).getDependency();
+    }
+
+    private DependencyNode path( DependencyNode root, int... coords )
+    {
+        try
+        {
+            DependencyNode node = root;
+            for ( int coord : coords )
+            {
+                node = node.getChildren().get( coord );
+            }
+
+            return node;
+        }
+        catch ( IndexOutOfBoundsException e )
+        {
+            throw new IllegalArgumentException( "illegal coordinates for child", e );
+        }
+        catch ( NullPointerException e )
+        {
+            throw new IllegalArgumentException( "illegal coordinates for child", e );
+        }
+    }
+
+    @Test
+    public void testSimpleCollection()
+        throws IOException, DependencyCollectionException
+    {
+        Dependency dependency = newDep( "gid:aid:ext:ver", "compile" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+        Dependency newDependency = root.getDependency();
+
+        assertEquals( dependency, newDependency );
+        assertEquals( dependency.getArtifact(), newDependency.getArtifact() );
+
+        assertEquals( 1, root.getChildren().size() );
+
+        Dependency expect = newDep( "gid:aid2:ext:ver", "compile" );
+        assertEquals( expect, root.getChildren().get( 0 ).getDependency() );
+    }
+
+    @Test
+    public void testMissingDependencyDescription()
+        throws IOException
+    {
+        CollectRequest request =
+            new CollectRequest( newDep( "missing:description:ext:ver" ), Arrays.asList( repository ) );
+        try
+        {
+            collector.collectDependencies( session, request );
+            fail( "expected exception" );
+        }
+        catch ( DependencyCollectionException e )
+        {
+            CollectResult result = e.getResult();
+            assertSame( request, result.getRequest() );
+            assertNotNull( result.getExceptions() );
+            assertEquals( 1, result.getExceptions().size() );
+
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactDescriptorException );
+
+            assertEquals( request.getRoot(), result.getRoot().getDependency() );
+        }
+    }
+
+    @Test
+    public void testDuplicates()
+        throws IOException, DependencyCollectionException
+    {
+        Dependency dependency = newDep( "duplicate:transitive:ext:dependency" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+        Dependency newDependency = root.getDependency();
+
+        assertEquals( dependency, newDependency );
+        assertEquals( dependency.getArtifact(), newDependency.getArtifact() );
+
+        assertEquals( 2, root.getChildren().size() );
+
+        Dependency dep = newDep( "gid:aid:ext:ver", "compile" );
+        assertEquals( dep, dep( root, 0 ) );
+
+        dep = newDep( "gid:aid2:ext:ver", "compile" );
+        assertEquals( dep, dep( root, 1 ) );
+        assertEquals( dep, dep( root, 0, 0 ) );
+        assertEquals( dep( root, 1 ), dep( root, 0, 0 ) );
+    }
+
+    @Test
+    public void testEqualSubtree()
+        throws IOException, DependencyCollectionException
+    {
+        DependencyNode root = parser.parseResource( "expectedSubtreeComparisonResult.txt" );
+        Dependency dependency = root.getDependency();
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEqualSubtree( root, result.getRoot() );
+    }
+
+    @Test
+    public void testCyclicDependencies()
+        throws Exception
+    {
+        DependencyNode root = parser.parseResource( "cycle.txt" );
+        CollectRequest request = new CollectRequest( root.getDependency(), Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEqualSubtree( root, result.getRoot() );
+    }
+
+    @Test
+    public void testCyclicDependenciesBig()
+        throws Exception
+    {
+        CollectRequest request = new CollectRequest( newDep( "1:2:pom:5.50-SNAPSHOT" ), Arrays.asList( repository ) );
+        collector.setArtifactDescriptorReader( newReader( "cycle-big/" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertNotNull( result.getRoot() );
+        // we only care about the performance here, this test must not hang or run out of mem
+    }
+
+    @Test
+    public void testCyclicProjects()
+        throws Exception
+    {
+        CollectRequest request = new CollectRequest( newDep( "test:a:2" ), Arrays.asList( repository ) );
+        collector.setArtifactDescriptorReader( newReader( "versionless-cycle/" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode root = result.getRoot();
+        DependencyNode a1 = path( root, 0, 0 );
+        assertEquals( "a", a1.getArtifact().getArtifactId() );
+        assertEquals( "1", a1.getArtifact().getVersion() );
+        for ( DependencyNode child : a1.getChildren() )
+        {
+            assertFalse( "1".equals( child.getArtifact().getVersion() ) );
+        }
+
+        assertEquals( 1, result.getCycles().size() );
+        DependencyCycle cycle = result.getCycles().get( 0 );
+        assertEquals( Arrays.asList(), cycle.getPrecedingDependencies() );
+        assertEquals( Arrays.asList( root.getDependency(), path( root, 0 ).getDependency(), a1.getDependency() ),
+                      cycle.getCyclicDependencies() );
+    }
+
+    @Test
+    public void testCyclicProjects_ConsiderLabelOfRootlessGraph()
+        throws Exception
+    {
+        Dependency dep = newDep( "gid:aid:ver", "compile" );
+        CollectRequest request =
+            new CollectRequest().addDependency( dep ).addRepository( repository ).setRootArtifact( dep.getArtifact() );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode root = result.getRoot();
+        DependencyNode a1 = root.getChildren().get( 0 );
+        assertEquals( "aid", a1.getArtifact().getArtifactId() );
+        assertEquals( "ver", a1.getArtifact().getVersion() );
+        DependencyNode a2 = a1.getChildren().get( 0 );
+        assertEquals( "aid2", a2.getArtifact().getArtifactId() );
+        assertEquals( "ver", a2.getArtifact().getVersion() );
+
+        assertEquals( 1, result.getCycles().size() );
+        DependencyCycle cycle = result.getCycles().get( 0 );
+        assertEquals( Arrays.asList(), cycle.getPrecedingDependencies() );
+        assertEquals( Arrays.asList( new Dependency( dep.getArtifact(), null ), a1.getDependency() ),
+                      cycle.getCyclicDependencies() );
+    }
+
+    @Test
+    public void testPartialResultOnError()
+        throws IOException
+    {
+        DependencyNode root = parser.parseResource( "expectedPartialSubtreeOnError.txt" );
+
+        Dependency dependency = root.getDependency();
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        CollectResult result;
+        try
+        {
+            result = collector.collectDependencies( session, request );
+            fail( "expected exception " );
+        }
+        catch ( DependencyCollectionException e )
+        {
+            result = e.getResult();
+
+            assertSame( request, result.getRequest() );
+            assertNotNull( result.getExceptions() );
+            assertEquals( 1, result.getExceptions().size() );
+
+            assertTrue( result.getExceptions().get( 0 ) instanceof ArtifactDescriptorException );
+
+            assertEqualSubtree( root, result.getRoot() );
+        }
+    }
+
+    @Test
+    public void testCollectMultipleDependencies()
+        throws IOException, DependencyCollectionException
+    {
+        Dependency root1 = newDep( "gid:aid:ext:ver", "compile" );
+        Dependency root2 = newDep( "gid:aid2:ext:ver", "compile" );
+        List<Dependency> dependencies = Arrays.asList( root1, root2 );
+        CollectRequest request = new CollectRequest( dependencies, null, Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+        assertEquals( 2, result.getRoot().getChildren().size() );
+        assertEquals( root1, dep( result.getRoot(), 0 ) );
+
+        assertEquals( 1, path( result.getRoot(), 0 ).getChildren().size() );
+        assertEquals( root2, dep( result.getRoot(), 0, 0 ) );
+
+        assertEquals( 0, path( result.getRoot(), 1 ).getChildren().size() );
+        assertEquals( root2, dep( result.getRoot(), 1 ) );
+    }
+
+    @Test
+    public void testArtifactDescriptorResolutionNotRestrictedToRepoHostingSelectedVersion()
+        throws Exception
+    {
+        RemoteRepository repo2 = new RemoteRepository.Builder( "test", "default", "file:///" ).build();
+
+        final List<RemoteRepository> repos = new ArrayList<RemoteRepository>();
+
+        collector.setArtifactDescriptorReader( new ArtifactDescriptorReader()
+        {
+            public ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session,
+                                                                    ArtifactDescriptorRequest request )
+                throws ArtifactDescriptorException
+            {
+                repos.addAll( request.getRepositories() );
+                return new ArtifactDescriptorResult( request );
+            }
+        } );
+
+        List<Dependency> dependencies = Arrays.asList( newDep( "verrange:parent:jar:1[1,)", "compile" ) );
+        CollectRequest request = new CollectRequest( dependencies, null, Arrays.asList( repository, repo2 ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+        assertEquals( 2, repos.size() );
+        assertEquals( "id", repos.get( 0 ).getId() );
+        assertEquals( "test", repos.get( 1 ).getId() );
+    }
+
+    @Test
+    public void testManagedVersionScope()
+        throws IOException, DependencyCollectionException
+    {
+        Dependency dependency = newDep( "managed:aid:ext:ver" );
+        CollectRequest request = new CollectRequest( dependency, Arrays.asList( repository ) );
+
+        session.setDependencyManager( new ClassicDependencyManager() );
+
+        CollectResult result = collector.collectDependencies( session, request );
+
+        assertEquals( 0, result.getExceptions().size() );
+
+        DependencyNode root = result.getRoot();
+
+        assertEquals( dependency, dep( root ) );
+        assertEquals( dependency.getArtifact(), dep( root ).getArtifact() );
+
+        assertEquals( 1, root.getChildren().size() );
+        Dependency expect = newDep( "gid:aid:ext:ver", "compile" );
+        assertEquals( expect, dep( root, 0 ) );
+
+        assertEquals( 1, path( root, 0 ).getChildren().size() );
+        expect = newDep( "gid:aid2:ext:managedVersion", "managedScope" );
+        assertEquals( expect, dep( root, 0, 0 ) );
+    }
+
+    @Test
+    public void testDependencyManagement()
+        throws IOException, DependencyCollectionException
+    {
+        collector.setArtifactDescriptorReader( newReader( "managed/" ) );
+
+        DependencyNode root = parser.parseResource( "expectedSubtreeComparisonResult.txt" );
+        TestDependencyManager depMgmt = new TestDependencyManager();
+        depMgmt.add( dep( root, 0 ), "managed", null, null );
+        depMgmt.add( dep( root, 0, 1 ), "managed", "managed", null );
+        depMgmt.add( dep( root, 1 ), null, null, "managed" );
+        session.setDependencyManager( depMgmt );
+
+        // collect result will differ from expectedSubtreeComparisonResult.txt
+        // set localPath -> no dependency traversal
+        CollectRequest request = new CollectRequest( dep( root ), Arrays.asList( repository ) );
+        CollectResult result = collector.collectDependencies( session, request );
+
+        DependencyNode node = result.getRoot();
+        assertEquals( "managed", dep( node, 0, 1 ).getArtifact().getVersion() );
+        assertEquals( "managed", dep( node, 0, 1 ).getScope() );
+
+        assertEquals( "managed", dep( node, 1 ).getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) );
+        assertEquals( "managed", dep( node, 0, 0 ).getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) );
+    }
+
+    @Test
+    public void testDependencyManagement_VerboseMode()
+        throws Exception
+    {
+        String depId = "gid:aid2:ext";
+        TestDependencyManager depMgmt = new TestDependencyManager();
+        depMgmt.version( depId, "managedVersion" );
+        depMgmt.scope( depId, "managedScope" );
+        depMgmt.optional( depId, Boolean.TRUE );
+        depMgmt.path( depId, "managedPath" );
+        depMgmt.exclusions( depId, new Exclusion( "gid", "aid", "*", "*" ) );
+        session.setDependencyManager( depMgmt );
+        session.setConfigProperty( DependencyManagerUtils.CONFIG_PROP_VERBOSE, Boolean.TRUE );
+
+        CollectRequest request = new CollectRequest().setRoot( newDep( "gid:aid:ver" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        DependencyNode node = result.getRoot().getChildren().get( 0 );
+        assertEquals( DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE | DependencyNode.MANAGED_OPTIONAL
+            | DependencyNode.MANAGED_PROPERTIES | DependencyNode.MANAGED_EXCLUSIONS, node.getManagedBits() );
+        assertEquals( "ver", DependencyManagerUtils.getPremanagedVersion( node ) );
+        assertEquals( "compile", DependencyManagerUtils.getPremanagedScope( node ) );
+        assertEquals( Boolean.FALSE, DependencyManagerUtils.getPremanagedOptional( node ) );
+    }
+
+    @Test
+    public void testVersionFilter()
+        throws Exception
+    {
+        session.setVersionFilter( new HighestVersionFilter() );
+        CollectRequest request = new CollectRequest().setRoot( newDep( "gid:aid:1" ) );
+        CollectResult result = collector.collectDependencies( session, request );
+        assertEquals( 1, result.getRoot().getChildren().size() );
+    }
+
+    static class TestDependencyManager
+        implements DependencyManager
+    {
+
+        private Map<String, String> versions = new HashMap<String, String>();
+
+        private Map<String, String> scopes = new HashMap<String, String>();
+
+        private Map<String, Boolean> optionals = new HashMap<String, Boolean>();
+
+        private Map<String, String> paths = new HashMap<String, String>();
+
+        private Map<String, Collection<Exclusion>> exclusions = new HashMap<String, Collection<Exclusion>>();
+
+        public void add( Dependency d, String version, String scope, String localPath )
+        {
+            String id = toKey( d );
+            version( id, version );
+            scope( id, scope );
+            path( id, localPath );
+        }
+
+        public void version( String id, String version )
+        {
+            versions.put( id, version );
+        }
+
+        public void scope( String id, String scope )
+        {
+            scopes.put( id, scope );
+        }
+
+        public void optional( String id, Boolean optional )
+        {
+            optionals.put( id, optional );
+        }
+
+        public void path( String id, String path )
+        {
+            paths.put( id, path );
+        }
+
+        public void exclusions( String id, Exclusion... exclusions )
+        {
+            this.exclusions.put( id, exclusions != null ? Arrays.asList( exclusions ) : null );
+        }
+
+        public DependencyManagement manageDependency( Dependency d )
+        {
+            String id = toKey( d );
+            DependencyManagement mgmt = new DependencyManagement();
+            mgmt.setVersion( versions.get( id ) );
+            mgmt.setScope( scopes.get( id ) );
+            mgmt.setOptional( optionals.get( id ) );
+            String path = paths.get( id );
+            if ( path != null )
+            {
+                mgmt.setProperties( Collections.singletonMap( ArtifactProperties.LOCAL_PATH, path ) );
+            }
+            mgmt.setExclusions( exclusions.get( id ) );
+            return mgmt;
+        }
+
+        private String toKey( Dependency dependency )
+        {
+            return ArtifactIdUtils.toVersionlessId( dependency.getArtifact() );
+        }
+
+        public DependencyManager deriveChildManager( DependencyCollectionContext context )
+        {
+            return this;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
new file mode 100644
index 0000000..9465e87
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
@@ -0,0 +1,385 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.deployment.DeployRequest;
+import org.eclipse.aether.deployment.DeploymentException;
+import org.eclipse.aether.internal.impl.DefaultDeployer;
+import org.eclipse.aether.internal.test.util.TestFileProcessor;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.MergeableMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.metadata.Metadata.Nature;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.ArtifactUpload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.MetadataUpload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultDeployerTest
+{
+
+    private Artifact artifact;
+
+    private DefaultMetadata metadata;
+
+    private DefaultRepositorySystemSession session;
+
+    private StubRepositoryConnectorProvider connectorProvider;
+
+    private DefaultDeployer deployer;
+
+    private DeployRequest request;
+
+    private RecordingRepositoryConnector connector;
+
+    private RecordingRepositoryListener listener;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        artifact = new DefaultArtifact( "gid", "aid", "jar", "ver" );
+        artifact = artifact.setFile( TestFileUtils.createTempFile( "artifact" ) );
+        metadata =
+            new DefaultMetadata( "gid", "aid", "ver", "type", Nature.RELEASE_OR_SNAPSHOT,
+                                 TestFileUtils.createTempFile( "metadata" ) );
+
+        session = TestUtils.newSession();
+        connectorProvider = new StubRepositoryConnectorProvider();
+
+        deployer = new DefaultDeployer();
+        deployer.setRepositoryConnectorProvider( connectorProvider );
+        deployer.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
+        deployer.setRepositoryEventDispatcher( new StubRepositoryEventDispatcher() );
+        deployer.setUpdateCheckManager( new StaticUpdateCheckManager( true ) );
+        deployer.setFileProcessor( new TestFileProcessor() );
+        deployer.setSyncContextFactory( new StubSyncContextFactory() );
+        deployer.setOfflineController( new DefaultOfflineController() );
+
+        request = new DeployRequest();
+        request.setRepository( new RemoteRepository.Builder( "id", "default", "file:///" ).build() );
+        connector = new RecordingRepositoryConnector( session );
+        connectorProvider.setConnector( connector );
+
+        listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        if ( session.getLocalRepository() != null )
+        {
+            TestFileUtils.deleteFile( session.getLocalRepository().getBasedir() );
+        }
+        session = null;
+        listener = null;
+        connector = null;
+        connectorProvider = null;
+        deployer = null;
+    }
+
+    @Test
+    public void testSuccessfulDeploy()
+        throws DeploymentException
+    {
+
+        connector.setExpectPut( artifact );
+        connector.setExpectPut( metadata );
+
+        request.addArtifact( artifact );
+        request.addMetadata( metadata );
+
+        deployer.deploy( session, request );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test( expected = DeploymentException.class )
+    public void testNullArtifactFile()
+        throws DeploymentException
+    {
+        request.addArtifact( artifact.setFile( null ) );
+        deployer.deploy( session, request );
+    }
+
+    @Test( expected = DeploymentException.class )
+    public void testNullMetadataFile()
+        throws DeploymentException
+    {
+        request.addArtifact( artifact.setFile( null ) );
+        deployer.deploy( session, request );
+    }
+
+    @Test
+    public void testSuccessfulArtifactEvents()
+        throws DeploymentException
+    {
+        request.addArtifact( artifact );
+
+        deployer.deploy( session, request );
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( 2, events.size() );
+
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( EventType.ARTIFACT_DEPLOYING, event.getType() );
+        assertEquals( artifact, event.getArtifact() );
+        assertNull( event.getException() );
+
+        event = events.get( 1 );
+        assertEquals( EventType.ARTIFACT_DEPLOYED, event.getType() );
+        assertEquals( artifact, event.getArtifact() );
+        assertNull( event.getException() );
+    }
+
+    @Test
+    public void testFailingArtifactEvents()
+    {
+        connector.fail = true;
+
+        request.addArtifact( artifact );
+
+        try
+        {
+            deployer.deploy( session, request );
+            fail( "expected exception" );
+        }
+        catch ( DeploymentException e )
+        {
+            List<RepositoryEvent> events = listener.getEvents();
+            assertEquals( 2, events.size() );
+
+            RepositoryEvent event = events.get( 0 );
+            assertEquals( EventType.ARTIFACT_DEPLOYING, event.getType() );
+            assertEquals( artifact, event.getArtifact() );
+            assertNull( event.getException() );
+
+            event = events.get( 1 );
+            assertEquals( EventType.ARTIFACT_DEPLOYED, event.getType() );
+            assertEquals( artifact, event.getArtifact() );
+            assertNotNull( event.getException() );
+        }
+    }
+
+    @Test
+    public void testSuccessfulMetadataEvents()
+        throws DeploymentException
+    {
+        request.addMetadata( metadata );
+
+        deployer.deploy( session, request );
+
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( 2, events.size() );
+
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( EventType.METADATA_DEPLOYING, event.getType() );
+        assertEquals( metadata, event.getMetadata() );
+        assertNull( event.getException() );
+
+        event = events.get( 1 );
+        assertEquals( EventType.METADATA_DEPLOYED, event.getType() );
+        assertEquals( metadata, event.getMetadata() );
+        assertNull( event.getException() );
+    }
+
+    @Test
+    public void testFailingMetdataEvents()
+    {
+        connector.fail = true;
+
+        request.addMetadata( metadata );
+
+        try
+        {
+            deployer.deploy( session, request );
+            fail( "expected exception" );
+        }
+        catch ( DeploymentException e )
+        {
+            List<RepositoryEvent> events = listener.getEvents();
+            assertEquals( 2, events.size() );
+
+            RepositoryEvent event = events.get( 0 );
+            assertEquals( EventType.METADATA_DEPLOYING, event.getType() );
+            assertEquals( metadata, event.getMetadata() );
+            assertNull( event.getException() );
+
+            event = events.get( 1 );
+            assertEquals( EventType.METADATA_DEPLOYED, event.getType() );
+            assertEquals( metadata, event.getMetadata() );
+            assertNotNull( event.getException() );
+        }
+    }
+
+    @Test
+    public void testStaleLocalMetadataCopyGetsDeletedBeforeMergeWhenMetadataIsNotCurrentlyPresentInRemoteRepo()
+        throws Exception
+    {
+        MergeableMetadata metadata = new MergeableMetadata()
+        {
+
+            public Metadata setFile( File file )
+            {
+                return this;
+            }
+
+            public String getVersion()
+            {
+                return "";
+            }
+
+            public String getType()
+            {
+                return "test.properties";
+            }
+
+            public Nature getNature()
+            {
+                return Nature.RELEASE;
+            }
+
+            public String getGroupId()
+            {
+                return "org";
+            }
+
+            public File getFile()
+            {
+                return null;
+            }
+
+            public String getArtifactId()
+            {
+                return "aether";
+            }
+
+            public Metadata setProperties( Map<String, String> properties )
+            {
+                return this;
+            }
+
+            public Map<String, String> getProperties()
+            {
+                return Collections.emptyMap();
+            }
+
+            public String getProperty( String key, String defaultValue )
+            {
+                return defaultValue;
+            }
+
+            public void merge( File current, File result )
+                throws RepositoryException
+            {
+                Properties props = new Properties();
+
+                try
+                {
+                    if ( current.isFile() )
+                    {
+                        TestFileUtils.readProps( current, props );
+                    }
+
+                    props.setProperty( "new", "value" );
+
+                    TestFileUtils.writeProps( result, props );
+                }
+                catch ( IOException e )
+                {
+                    throw new RepositoryException( e.getMessage(), e );
+                }
+            }
+
+            public boolean isMerged()
+            {
+                return false;
+            }
+        };
+
+        connectorProvider.setConnector( new RepositoryConnector()
+        {
+
+            public void put( Collection<? extends ArtifactUpload> artifactUploads,
+                             Collection<? extends MetadataUpload> metadataUploads )
+            {
+            }
+
+            public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                             Collection<? extends MetadataDownload> metadataDownloads )
+            {
+                if ( metadataDownloads != null )
+                {
+                    for ( MetadataDownload download : metadataDownloads )
+                    {
+                        download.setException( new MetadataNotFoundException( download.getMetadata(), null, null ) );
+                    }
+                }
+            }
+
+            public void close()
+            {
+            }
+        } );
+
+        request.addMetadata( metadata );
+
+        File metadataFile =
+            new File( session.getLocalRepository().getBasedir(),
+                      session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata, request.getRepository(),
+                                                                                    "" ) );
+        Properties props = new Properties();
+        props.setProperty( "old", "value" );
+        TestFileUtils.writeProps( metadataFile, props );
+
+        deployer.deploy( session, request );
+
+        props = new Properties();
+        TestFileUtils.readProps( metadataFile, props );
+        assertNull( props.toString(), props.get( "old" ) );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultFileProcessorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultFileProcessorTest.java
new file mode 100644
index 0000000..7b48230
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultFileProcessorTest.java
@@ -0,0 +1,128 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.aether.internal.impl.DefaultFileProcessor;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.spi.io.FileProcessor.ProgressListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultFileProcessorTest
+{
+
+    private File targetDir;
+
+    private DefaultFileProcessor fileProcessor;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        targetDir = TestFileUtils.createTempDir( getClass().getSimpleName() );
+        fileProcessor = new DefaultFileProcessor();
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( targetDir );
+        fileProcessor = null;
+    }
+
+    @Test
+    public void testCopy()
+        throws IOException
+    {
+        String data = "testCopy\nasdf";
+        File file = TestFileUtils.createTempFile( data );
+        File target = new File( targetDir, "testCopy.txt" );
+
+        fileProcessor.copy( file, target );
+
+        assertEquals( data, TestFileUtils.readString( file ) );
+
+        file.delete();
+    }
+
+    @Test
+    public void testOverwrite()
+        throws IOException
+    {
+        String data = "testCopy\nasdf";
+        File file = TestFileUtils.createTempFile( data );
+
+        for ( int i = 0; i < 5; i++ )
+        {
+            File target = new File( targetDir, "testCopy.txt" );
+            fileProcessor.copy( file, target );
+            assertEquals( data, TestFileUtils.readString( file ) );
+        }
+
+        file.delete();
+    }
+
+    @Test
+    public void testCopyEmptyFile()
+        throws IOException
+    {
+        File file = TestFileUtils.createTempFile( "" );
+        File target = new File( targetDir, "testCopyEmptyFile" );
+        target.delete();
+        fileProcessor.copy( file, target );
+        assertTrue( "empty file was not copied", target.exists() && target.length() == 0L );
+        target.delete();
+    }
+
+    @Test
+    public void testProgressingChannel()
+        throws IOException
+    {
+        File file = TestFileUtils.createTempFile( "test" );
+        File target = new File( targetDir, "testProgressingChannel" );
+        target.delete();
+        final AtomicInteger progressed = new AtomicInteger();
+        ProgressListener listener = new ProgressListener()
+        {
+            public void progressed( ByteBuffer buffer )
+                throws IOException
+            {
+                progressed.addAndGet( buffer.remaining() );
+            }
+        };
+        fileProcessor.copy( file, target, listener );
+        assertTrue( "file was not created", target.isFile() );
+        assertEquals( "file was not fully copied", 4L, target.length() );
+        assertEquals( "listener not called", 4, progressed.intValue() );
+        target.delete();
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
new file mode 100644
index 0000000..efabedd
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
@@ -0,0 +1,413 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryEvent.EventType;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.installation.InstallRequest;
+import org.eclipse.aether.installation.InstallResult;
+import org.eclipse.aether.installation.InstallationException;
+import org.eclipse.aether.internal.impl.DefaultFileProcessor;
+import org.eclipse.aether.internal.impl.DefaultInstaller;
+import org.eclipse.aether.internal.test.util.TestFileProcessor;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.metadata.Metadata.Nature;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultInstallerTest
+{
+
+    private Artifact artifact;
+
+    private Metadata metadata;
+
+    private DefaultRepositorySystemSession session;
+
+    private String localArtifactPath;
+
+    private String localMetadataPath;
+
+    private DefaultInstaller installer;
+
+    private InstallRequest request;
+
+    private RecordingRepositoryListener listener;
+
+    private File localArtifactFile;
+
+    private TestLocalRepositoryManager lrm;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        artifact = new DefaultArtifact( "gid", "aid", "jar", "ver" );
+        artifact = artifact.setFile( TestFileUtils.createTempFile( "artifact".getBytes(), 1 ) );
+        metadata =
+            new DefaultMetadata( "gid", "aid", "ver", "type", Nature.RELEASE_OR_SNAPSHOT,
+                                 TestFileUtils.createTempFile( "metadata".getBytes(), 1 ) );
+
+        session = TestUtils.newSession();
+        localArtifactPath = session.getLocalRepositoryManager().getPathForLocalArtifact( artifact );
+        localMetadataPath = session.getLocalRepositoryManager().getPathForLocalMetadata( metadata );
+
+        localArtifactFile = new File( session.getLocalRepository().getBasedir(), localArtifactPath );
+
+        installer = new DefaultInstaller();
+        installer.setFileProcessor( new TestFileProcessor() );
+        installer.setRepositoryEventDispatcher( new StubRepositoryEventDispatcher() );
+        installer.setSyncContextFactory( new StubSyncContextFactory() );
+        request = new InstallRequest();
+        listener = new RecordingRepositoryListener();
+        session.setRepositoryListener( listener );
+
+        lrm = (TestLocalRepositoryManager) session.getLocalRepositoryManager();
+
+        TestFileUtils.deleteFile( session.getLocalRepository().getBasedir() );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( session.getLocalRepository().getBasedir() );
+    }
+
+    @Test
+    public void testSuccessfulInstall()
+        throws InstallationException, IOException
+    {
+        File artifactFile =
+            new File( session.getLocalRepositoryManager().getRepository().getBasedir(), localArtifactPath );
+        File metadataFile =
+            new File( session.getLocalRepositoryManager().getRepository().getBasedir(), localMetadataPath );
+
+        artifactFile.delete();
+        metadataFile.delete();
+
+        request.addArtifact( artifact );
+        request.addMetadata( metadata );
+
+        InstallResult result = installer.install( session, request );
+
+        assertTrue( artifactFile.exists() );
+        assertEquals( "artifact", TestFileUtils.readString( artifactFile ) );
+
+        assertTrue( metadataFile.exists() );
+        assertEquals( "metadata", TestFileUtils.readString( metadataFile ) );
+
+        assertEquals( result.getRequest(), request );
+
+        assertEquals( result.getArtifacts().size(), 1 );
+        assertTrue( result.getArtifacts().contains( artifact ) );
+
+        assertEquals( result.getMetadata().size(), 1 );
+        assertTrue( result.getMetadata().contains( metadata ) );
+
+        assertEquals( 1, lrm.getMetadataRegistration().size() );
+        assertTrue( lrm.getMetadataRegistration().contains( metadata ) );
+        assertEquals( 1, lrm.getArtifactRegistration().size() );
+        assertTrue( lrm.getArtifactRegistration().contains( artifact ) );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testNullArtifactFile()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addArtifact( artifact.setFile( null ) );
+
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testNullMetadataFile()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addMetadata( metadata.setFile( null ) );
+
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testNonExistentArtifactFile()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addArtifact( artifact.setFile( new File( "missing.txt" ) ) );
+
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testNonExistentMetadataFile()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addMetadata( metadata.setFile( new File( "missing.xml" ) ) );
+
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testArtifactExistsAsDir()
+        throws InstallationException
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalArtifact( artifact );
+        File file = new File( session.getLocalRepository().getBasedir(), path );
+        assertFalse( file.getAbsolutePath() + " is a file, not directory", file.isFile() );
+        assertFalse( file.getAbsolutePath() + " already exists", file.exists() );
+        assertTrue( "failed to setup test: could not create " + file.getAbsolutePath(),
+                    file.mkdirs() || file.isDirectory() );
+
+        request.addArtifact( artifact );
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testMetadataExistsAsDir()
+        throws InstallationException
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalMetadata( metadata );
+        assertTrue( "failed to setup test: could not create " + path,
+                    new File( session.getLocalRepository().getBasedir(), path ).mkdirs() );
+
+        request.addMetadata( metadata );
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testArtifactDestinationEqualsSource()
+        throws Exception
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalArtifact( artifact );
+        File file = new File( session.getLocalRepository().getBasedir(), path );
+        artifact = artifact.setFile( file );
+        TestFileUtils.writeString( file, "test" );
+
+        request.addArtifact( artifact );
+        installer.install( session, request );
+    }
+
+    @Test( expected = InstallationException.class )
+    public void testMetadataDestinationEqualsSource()
+        throws Exception
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalMetadata( metadata );
+        File file = new File( session.getLocalRepository().getBasedir(), path );
+        metadata = metadata.setFile( file );
+        TestFileUtils.writeString( file, "test" );
+
+        request.addMetadata( metadata );
+        installer.install( session, request );
+    }
+
+    @Test
+    public void testSuccessfulArtifactEvents()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addArtifact( artifact );
+
+        installer.install( session, request );
+        checkEvents( "Repository Event problem", artifact, false );
+    }
+
+    @Test
+    public void testSuccessfulMetadataEvents()
+        throws InstallationException
+    {
+        InstallRequest request = new InstallRequest();
+        request.addMetadata( metadata );
+
+        installer.install( session, request );
+        checkEvents( "Repository Event problem", metadata, false );
+    }
+
+    @Test
+    public void testFailingEventsNullArtifactFile()
+    {
+        checkFailedEvents( "null artifact file", this.artifact.setFile( null ) );
+    }
+
+    @Test
+    public void testFailingEventsNullMetadataFile()
+    {
+        checkFailedEvents( "null metadata file", this.metadata.setFile( null ) );
+    }
+
+    @Test
+    public void testFailingEventsArtifactExistsAsDir()
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalArtifact( artifact );
+        assertTrue( "failed to setup test: could not create " + path,
+                    new File( session.getLocalRepository().getBasedir(), path ).mkdirs() );
+        checkFailedEvents( "target exists as dir", artifact );
+    }
+
+    @Test
+    public void testFailingEventsMetadataExistsAsDir()
+    {
+        String path = session.getLocalRepositoryManager().getPathForLocalMetadata( metadata );
+        assertTrue( "failed to setup test: could not create " + path,
+                    new File( session.getLocalRepository().getBasedir(), path ).mkdirs() );
+        checkFailedEvents( "target exists as dir", metadata );
+    }
+
+    private void checkFailedEvents( String msg, Metadata metadata )
+    {
+        InstallRequest request = new InstallRequest().addMetadata( metadata );
+        msg = "Repository events problem (case: " + msg + ")";
+
+        try
+        {
+            installer.install( session, request );
+            fail( "expected exception" );
+        }
+        catch ( InstallationException e )
+        {
+            checkEvents( msg, metadata, true );
+        }
+
+    }
+
+    private void checkEvents( String msg, Metadata metadata, boolean failed )
+    {
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( msg, 2, events.size() );
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( msg, EventType.METADATA_INSTALLING, event.getType() );
+        assertEquals( msg, metadata, event.getMetadata() );
+        assertNull( msg, event.getException() );
+
+        event = events.get( 1 );
+        assertEquals( msg, EventType.METADATA_INSTALLED, event.getType() );
+        assertEquals( msg, metadata, event.getMetadata() );
+        if ( failed )
+        {
+            assertNotNull( msg, event.getException() );
+        }
+        else
+        {
+            assertNull( msg, event.getException() );
+        }
+    }
+
+    private void checkFailedEvents( String msg, Artifact artifact )
+    {
+        InstallRequest request = new InstallRequest().addArtifact( artifact );
+        msg = "Repository events problem (case: " + msg + ")";
+
+        try
+        {
+            installer.install( session, request );
+            fail( "expected exception" );
+        }
+        catch ( InstallationException e )
+        {
+            checkEvents( msg, artifact, true );
+        }
+    }
+
+    private void checkEvents( String msg, Artifact artifact, boolean failed )
+    {
+        List<RepositoryEvent> events = listener.getEvents();
+        assertEquals( msg, 2, events.size() );
+        RepositoryEvent event = events.get( 0 );
+        assertEquals( msg, EventType.ARTIFACT_INSTALLING, event.getType() );
+        assertEquals( msg, artifact, event.getArtifact() );
+        assertNull( msg, event.getException() );
+        
+        event = events.get( 1 );
+        assertEquals( msg, EventType.ARTIFACT_INSTALLED, event.getType() );
+        assertEquals( msg, artifact, event.getArtifact() );
+        if ( failed )
+        {
+            assertNotNull( msg + " > expected exception", event.getException() );
+        }
+        else
+        {
+            assertNull( msg + " > " + event.getException(), event.getException() );
+        }
+    }
+
+    @Test
+    public void testDoNotUpdateUnchangedArtifact()
+        throws InstallationException
+    {
+        request.addArtifact( artifact );
+        installer.install( session, request );
+
+        installer.setFileProcessor( new DefaultFileProcessor()
+        {
+            @Override
+            public long copy( File src, File target, ProgressListener listener )
+                throws IOException
+            {
+                throw new IOException( "copy called" );
+            }
+        } );
+
+        request = new InstallRequest();
+        request.addArtifact( artifact );
+        installer.install( session, request );
+    }
+
+    @Test
+    public void testSetArtifactTimestamps()
+        throws InstallationException
+    {
+        artifact.getFile().setLastModified( artifact.getFile().lastModified() - 60000 );
+
+        request.addArtifact( artifact );
+
+        installer.install( session, request );
+
+        assertEquals( "artifact timestamp was not set to src file", artifact.getFile().lastModified(),
+                      localArtifactFile.lastModified() );
+
+        request = new InstallRequest();
+
+        request.addArtifact( artifact );
+
+        artifact.getFile().setLastModified( artifact.getFile().lastModified() - 60000 );
+
+        installer.install( session, request );
+
+        assertEquals( "artifact timestamp was not set to src file", artifact.getFile().lastModified(),
+                      localArtifactFile.lastModified() );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java
new file mode 100644
index 0000000..d977a78
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java
@@ -0,0 +1,256 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.impl.DefaultMetadataResolver;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.MetadataRequest;
+import org.eclipse.aether.resolution.MetadataResult;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultMetadataResolverTest
+{
+
+    private DefaultMetadataResolver resolver;
+
+    private StubRepositoryConnectorProvider connectorProvider;
+
+    private RemoteRepository repository;
+
+    private DefaultRepositorySystemSession session;
+
+    private Metadata metadata;
+
+    private RecordingRepositoryConnector connector;
+
+    private TestLocalRepositoryManager lrm;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        lrm = (TestLocalRepositoryManager) session.getLocalRepositoryManager();
+        connectorProvider = new StubRepositoryConnectorProvider();
+        resolver = new DefaultMetadataResolver();
+        resolver.setUpdateCheckManager( new StaticUpdateCheckManager( true ) );
+        resolver.setRepositoryEventDispatcher( new StubRepositoryEventDispatcher() );
+        resolver.setRepositoryConnectorProvider( connectorProvider );
+        resolver.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
+        resolver.setSyncContextFactory( new StubSyncContextFactory() );
+        resolver.setOfflineController( new DefaultOfflineController() );
+        repository =
+            new RemoteRepository.Builder( "test-DMRT", "default",
+                                          TestFileUtils.createTempDir().toURI().toURL().toString() ).build();
+        metadata = new DefaultMetadata( "gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        connector = new RecordingRepositoryConnector();
+        connectorProvider.setConnector( connector );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( new File( new URI( repository.getUrl() ) ) );
+        TestFileUtils.deleteFile( session.getLocalRepository().getBasedir() );
+    }
+
+    @Test
+    public void testNoRepositoryFailing()
+    {
+        MetadataRequest request = new MetadataRequest( metadata, null, "" );
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+
+        assertEquals( 1, results.size() );
+
+        MetadataResult result = results.get( 0 );
+        assertSame( request, result.getRequest() );
+        assertNotNull( "" + ( result.getMetadata() != null ? result.getMetadata().getFile() : result.getMetadata() ),
+                       result.getException() );
+        assertEquals( MetadataNotFoundException.class, result.getException().getClass() );
+
+        assertNull( result.getMetadata() );
+    }
+
+    @Test
+    public void testResolve()
+        throws IOException
+    {
+        connector.setExpectGet( metadata );
+
+        // prepare "download"
+        File file =
+            new File( session.getLocalRepository().getBasedir(),
+                      session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata, repository, "" ) );
+
+        TestFileUtils.writeString( file, file.getAbsolutePath() );
+
+        MetadataRequest request = new MetadataRequest( metadata, repository, "" );
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+
+        assertEquals( 1, results.size() );
+
+        MetadataResult result = results.get( 0 );
+        assertSame( request, result.getRequest() );
+        assertNull( result.getException() );
+        assertNotNull( result.getMetadata() );
+        assertNotNull( result.getMetadata().getFile() );
+
+        assertEquals( file, result.getMetadata().getFile() );
+        assertEquals( metadata, result.getMetadata().setFile( null ) );
+
+        connector.assertSeenExpected();
+        Set<Metadata> metadataRegistration =
+            ( (TestLocalRepositoryManager) session.getLocalRepositoryManager() ).getMetadataRegistration();
+        assertTrue( metadataRegistration.contains( metadata ) );
+        assertEquals( 1, metadataRegistration.size() );
+    }
+
+    @Test
+    public void testRemoveMetadataIfMissing()
+        throws IOException
+    {
+        connector = new RecordingRepositoryConnector()
+        {
+
+            @Override
+            public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                             Collection<? extends MetadataDownload> metadataDownloads )
+            {
+                super.get( artifactDownloads, metadataDownloads );
+                for ( MetadataDownload d : metadataDownloads )
+                {
+                    d.setException( new MetadataNotFoundException( metadata, repository ) );
+                }
+            }
+
+        };
+        connectorProvider.setConnector( connector );
+
+        File file =
+            new File( session.getLocalRepository().getBasedir(),
+                      session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata, repository, "" ) );
+        TestFileUtils.writeString( file, file.getAbsolutePath() );
+        metadata.setFile( file );
+
+        MetadataRequest request = new MetadataRequest( metadata, repository, "" );
+        request.setDeleteLocalCopyIfMissing( true );
+
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+        assertEquals( 1, results.size() );
+        MetadataResult result = results.get( 0 );
+
+        assertNotNull( result.getException() );
+        assertEquals( false, file.exists() );
+    }
+
+    @Test
+    public void testOfflineSessionResolveMetadataMissing()
+    {
+        session.setOffline( true );
+        MetadataRequest request = new MetadataRequest( metadata, repository, "" );
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+
+        assertEquals( 1, results.size() );
+
+        MetadataResult result = results.get( 0 );
+        assertSame( request, result.getRequest() );
+        assertNotNull( result.getException() );
+        assertNull( result.getMetadata() );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test
+    public void testOfflineSessionResolveMetadata()
+        throws IOException
+    {
+        session.setOffline( true );
+
+        String path = session.getLocalRepositoryManager().getPathForRemoteMetadata( metadata, repository, "" );
+        File file = new File( session.getLocalRepository().getBasedir(), path );
+        TestFileUtils.writeString( file, file.getAbsolutePath() );
+
+        // set file to use in TestLRM find()
+        metadata = metadata.setFile( file );
+
+        MetadataRequest request = new MetadataRequest( metadata, repository, "" );
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+
+        assertEquals( 1, results.size() );
+        MetadataResult result = results.get( 0 );
+        assertSame( request, result.getRequest() );
+        assertNull( String.valueOf( result.getException() ), result.getException() );
+        assertNotNull( result.getMetadata() );
+        assertNotNull( result.getMetadata().getFile() );
+
+        assertEquals( file, result.getMetadata().getFile() );
+        assertEquals( metadata.setFile( null ), result.getMetadata().setFile( null ) );
+
+        connector.assertSeenExpected();
+    }
+
+    @Test
+    public void testFavorLocal()
+        throws IOException
+    {
+        lrm.add( session, new LocalMetadataRegistration( metadata ) );
+        String path = session.getLocalRepositoryManager().getPathForLocalMetadata( metadata );
+        File file = new File( session.getLocalRepository().getBasedir(), path );
+        TestFileUtils.writeString( file, file.getAbsolutePath() );
+
+        MetadataRequest request = new MetadataRequest( metadata, repository, "" );
+        request.setFavorLocalRepository( true );
+        resolver.setUpdateCheckManager( new StaticUpdateCheckManager( true, true ) );
+
+        List<MetadataResult> results = resolver.resolveMetadata( session, Arrays.asList( request ) );
+
+        assertEquals( 1, results.size() );
+        MetadataResult result = results.get( 0 );
+        assertSame( request, result.getRequest() );
+        assertNull( String.valueOf( result.getException() ), result.getException() );
+
+        connector.assertSeenExpected();
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultOfflineControllerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultOfflineControllerTest.java
new file mode 100644
index 0000000..7e42707
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultOfflineControllerTest.java
@@ -0,0 +1,102 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultOfflineControllerTest
+{
+
+    private DefaultOfflineController controller;
+
+    private RepositorySystemSession newSession( boolean offline, String protocols, String hosts )
+    {
+        DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+        session.setOffline( offline );
+        session.setConfigProperty( DefaultOfflineController.CONFIG_PROP_OFFLINE_PROTOCOLS, protocols );
+        session.setConfigProperty( DefaultOfflineController.CONFIG_PROP_OFFLINE_HOSTS, hosts );
+        return session;
+    }
+
+    private RemoteRepository newRepo( String url )
+    {
+        return new RemoteRepository.Builder( "central", "default", url ).build();
+    }
+
+    @Before
+    public void setup()
+    {
+        controller = new DefaultOfflineController();
+    }
+
+    @Test( expected = RepositoryOfflineException.class )
+    public void testCheckOffline_Online()
+        throws Exception
+    {
+        controller.checkOffline( newSession( false, null, null ), newRepo( "http://eclipse.org" ) );
+    }
+
+    @Test( expected = RepositoryOfflineException.class )
+    public void testCheckOffline_Offline()
+        throws Exception
+    {
+        controller.checkOffline( newSession( true, null, null ), newRepo( "http://eclipse.org" ) );
+    }
+
+    @Test
+    public void testCheckOffline_Offline_OfflineProtocol()
+        throws Exception
+    {
+        controller.checkOffline( newSession( true, "file", null ), newRepo( "file://repo" ) );
+        controller.checkOffline( newSession( true, "file", null ), newRepo( "FILE://repo" ) );
+        controller.checkOffline( newSession( true, "  file  ,  classpath  ", null ), newRepo( "file://repo" ) );
+        controller.checkOffline( newSession( true, "  file  ,  classpath  ", null ), newRepo( "classpath://repo" ) );
+    }
+
+    @Test( expected = RepositoryOfflineException.class )
+    public void testCheckOffline_Offline_OnlineProtocol()
+        throws Exception
+    {
+        controller.checkOffline( newSession( true, "file", null ), newRepo( "http://eclipse.org" ) );
+    }
+
+    @Test
+    public void testCheckOffline_Offline_OfflineHost()
+        throws Exception
+    {
+        controller.checkOffline( newSession( true, null, "localhost" ), newRepo( "http://localhost" ) );
+        controller.checkOffline( newSession( true, null, "localhost" ), newRepo( "http://LOCALHOST" ) );
+        controller.checkOffline( newSession( true, null, "  localhost  ,  127.0.0.1  " ), newRepo( "http://localhost" ) );
+        controller.checkOffline( newSession( true, null, "  localhost  ,  127.0.0.1  " ), newRepo( "http://127.0.0.1" ) );
+    }
+
+    @Test( expected = RepositoryOfflineException.class )
+    public void testCheckOffline_Offline_OnlineHost()
+        throws Exception
+    {
+        controller.checkOffline( newSession( true, null, "localhost" ), newRepo( "http://eclipse.org" ) );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
new file mode 100644
index 0000000..8bc50d6
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
@@ -0,0 +1,308 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
+import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class DefaultRemoteRepositoryManagerTest
+{
+
+    private DefaultRepositorySystemSession session;
+
+    private DefaultRemoteRepositoryManager manager;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        session.setChecksumPolicy( null );
+        session.setUpdatePolicy( null );
+        manager = new DefaultRemoteRepositoryManager();
+        manager.setUpdatePolicyAnalyzer( new StubUpdatePolicyAnalyzer() );
+        manager.setChecksumPolicyProvider( new DefaultChecksumPolicyProvider() );
+        manager.setLoggerFactory( new TestLoggerFactory() );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        manager = null;
+        session = null;
+    }
+
+    private RemoteRepository.Builder newRepo( String id, String url, boolean enabled, String updates, String checksums )
+    {
+        RepositoryPolicy policy = new RepositoryPolicy( enabled, updates, checksums );
+        return new RemoteRepository.Builder( id, "test", url ).setPolicy( policy );
+    }
+
+    private void assertEqual( RemoteRepository expected, RemoteRepository actual )
+    {
+        assertEquals( "id", expected.getId(), actual.getId() );
+        assertEquals( "url", expected.getUrl(), actual.getUrl() );
+        assertEquals( "type", expected.getContentType(), actual.getContentType() );
+        assertEqual( expected.getPolicy( false ), actual.getPolicy( false ) );
+        assertEqual( expected.getPolicy( true ), actual.getPolicy( true ) );
+    }
+
+    private void assertEqual( RepositoryPolicy expected, RepositoryPolicy actual )
+    {
+        assertEquals( "enabled", expected.isEnabled(), actual.isEnabled() );
+        assertEquals( "checksums", expected.getChecksumPolicy(), actual.getChecksumPolicy() );
+        assertEquals( "updates", expected.getUpdatePolicy(), actual.getUpdatePolicy() );
+    }
+
+    @Test
+    public void testGetPolicy()
+    {
+        RepositoryPolicy snapshotPolicy =
+            new RepositoryPolicy( true, RepositoryPolicy.UPDATE_POLICY_ALWAYS, RepositoryPolicy.CHECKSUM_POLICY_IGNORE );
+        RepositoryPolicy releasePolicy =
+            new RepositoryPolicy( true, RepositoryPolicy.UPDATE_POLICY_NEVER, RepositoryPolicy.CHECKSUM_POLICY_FAIL );
+
+        RemoteRepository repo = new RemoteRepository.Builder( "id", "type", "http://localhost" ) //
+        .setSnapshotPolicy( snapshotPolicy ).setReleasePolicy( releasePolicy ).build();
+
+        RepositoryPolicy effectivePolicy = manager.getPolicy( session, repo, true, true );
+        assertEquals( true, effectivePolicy.isEnabled() );
+        assertEquals( RepositoryPolicy.CHECKSUM_POLICY_IGNORE, effectivePolicy.getChecksumPolicy() );
+        assertEquals( RepositoryPolicy.UPDATE_POLICY_ALWAYS, effectivePolicy.getUpdatePolicy() );
+    }
+
+    @Test
+    public void testAggregateSimpleRepos()
+    {
+        RemoteRepository dominant1 = newRepo( "a", "file://", false, "", "" ).build();
+
+        RemoteRepository recessive1 = newRepo( "a", "http://", true, "", "" ).build();
+        RemoteRepository recessive2 = newRepo( "b", "file://", true, "", "" ).build();
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Arrays.asList( dominant1 ),
+                                           Arrays.asList( recessive1, recessive2 ), false );
+
+        assertEquals( 2, result.size() );
+        assertEqual( dominant1, result.get( 0 ) );
+        assertEqual( recessive2, result.get( 1 ) );
+    }
+
+    @Test
+    public void testAggregateSimpleRepos_MustKeepDisabledRecessiveRepo()
+    {
+        RemoteRepository dominant = newRepo( "a", "file://", true, "", "" ).build();
+
+        RemoteRepository recessive1 = newRepo( "b", "http://", false, "", "" ).build();
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Arrays.asList( dominant ), Arrays.asList( recessive1 ), false );
+
+        RemoteRepository recessive2 = newRepo( recessive1.getId(), "http://", true, "", "" ).build();
+
+        result = manager.aggregateRepositories( session, result, Arrays.asList( recessive2 ), false );
+
+        assertEquals( 2, result.size() );
+        assertEqual( dominant, result.get( 0 ) );
+        assertEqual( recessive1, result.get( 1 ) );
+    }
+
+    @Test
+    public void testAggregateMirrorRepos_DominantMirrorComplete()
+    {
+        RemoteRepository dominant1 = newRepo( "a", "http://", false, "", "" ).build();
+        RemoteRepository dominantMirror1 =
+            newRepo( "x", "file://", false, "", "" ).addMirroredRepository( dominant1 ).build();
+
+        RemoteRepository recessive1 = newRepo( "a", "https://", true, "", "" ).build();
+        RemoteRepository recessiveMirror1 =
+            newRepo( "x", "http://", true, "", "" ).addMirroredRepository( recessive1 ).build();
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Arrays.asList( dominantMirror1 ),
+                                           Arrays.asList( recessiveMirror1 ), false );
+
+        assertEquals( 1, result.size() );
+        assertEqual( dominantMirror1, result.get( 0 ) );
+        assertEquals( 1, result.get( 0 ).getMirroredRepositories().size() );
+        assertEquals( dominant1, result.get( 0 ).getMirroredRepositories().get( 0 ) );
+    }
+
+    @Test
+    public void testAggregateMirrorRepos_DominantMirrorIncomplete()
+    {
+        RemoteRepository dominant1 = newRepo( "a", "http://", false, "", "" ).build();
+        RemoteRepository dominantMirror1 =
+            newRepo( "x", "file://", false, "", "" ).addMirroredRepository( dominant1 ).build();
+
+        RemoteRepository recessive1 = newRepo( "a", "https://", true, "", "" ).build();
+        RemoteRepository recessive2 = newRepo( "b", "https://", true, "", "" ).build();
+        RemoteRepository recessiveMirror1 =
+            newRepo( "x", "http://", true, "", "" ).setMirroredRepositories( Arrays.asList( recessive1, recessive2 ) ).build();
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Arrays.asList( dominantMirror1 ),
+                                           Arrays.asList( recessiveMirror1 ), false );
+
+        assertEquals( 1, result.size() );
+        assertEqual( newRepo( "x", "file://", true, "", "" ).build(), result.get( 0 ) );
+        assertEquals( 2, result.get( 0 ).getMirroredRepositories().size() );
+        assertEquals( dominant1, result.get( 0 ).getMirroredRepositories().get( 0 ) );
+        assertEquals( recessive2, result.get( 0 ).getMirroredRepositories().get( 1 ) );
+    }
+
+    @Test
+    public void testMirrorAuthentication()
+    {
+        final RemoteRepository repo = newRepo( "a", "http://", true, "", "" ).build();
+        final RemoteRepository mirror =
+            newRepo( "a", "http://", true, "", "" ).setAuthentication( new AuthenticationBuilder().addUsername( "test" ).build() ).build();
+        session.setMirrorSelector( new MirrorSelector()
+        {
+            public RemoteRepository getMirror( RemoteRepository repository )
+            {
+                return mirror;
+            }
+        } );
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Collections.<RemoteRepository> emptyList(), Arrays.asList( repo ),
+                                           true );
+
+        assertEquals( 1, result.size() );
+        assertSame( mirror.getAuthentication(), result.get( 0 ).getAuthentication() );
+    }
+
+    @Test
+    public void testMirrorProxy()
+    {
+        final RemoteRepository repo = newRepo( "a", "http://", true, "", "" ).build();
+        final RemoteRepository mirror =
+            newRepo( "a", "http://", true, "", "" ).setProxy( new Proxy( "http", "host", 2011, null ) ).build();
+        session.setMirrorSelector( new MirrorSelector()
+        {
+            public RemoteRepository getMirror( RemoteRepository repository )
+            {
+                return mirror;
+            }
+        } );
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Collections.<RemoteRepository> emptyList(), Arrays.asList( repo ),
+                                           true );
+
+        assertEquals( 1, result.size() );
+        assertEquals( "http", result.get( 0 ).getProxy().getType() );
+        assertEquals( "host", result.get( 0 ).getProxy().getHost() );
+        assertEquals( 2011, result.get( 0 ).getProxy().getPort() );
+    }
+
+    @Test
+    public void testProxySelector()
+    {
+        final RemoteRepository repo = newRepo( "a", "http://", true, "", "" ).build();
+        final Proxy proxy = new Proxy( "http", "host", 2011, null );
+        session.setProxySelector( new ProxySelector()
+        {
+            public Proxy getProxy( RemoteRepository repository )
+            {
+                return proxy;
+            }
+        } );
+        session.setMirrorSelector( new MirrorSelector()
+        {
+            public RemoteRepository getMirror( RemoteRepository repository )
+            {
+                return null;
+            }
+        } );
+
+        List<RemoteRepository> result =
+            manager.aggregateRepositories( session, Collections.<RemoteRepository> emptyList(), Arrays.asList( repo ),
+                                           true );
+
+        assertEquals( 1, result.size() );
+        assertEquals( "http", result.get( 0 ).getProxy().getType() );
+        assertEquals( "host", result.get( 0 ).getProxy().getHost() );
+        assertEquals( 2011, result.get( 0 ).getProxy().getPort() );
+    }
+
+    private static class StubUpdatePolicyAnalyzer
+        implements UpdatePolicyAnalyzer
+    {
+
+        public String getEffectiveUpdatePolicy( RepositorySystemSession session, String policy1, String policy2 )
+        {
+            return ordinalOfUpdatePolicy( policy1 ) < ordinalOfUpdatePolicy( policy2 ) ? policy1 : policy2;
+        }
+
+        private int ordinalOfUpdatePolicy( String policy )
+        {
+            if ( RepositoryPolicy.UPDATE_POLICY_DAILY.equals( policy ) )
+            {
+                return 1440;
+            }
+            else if ( RepositoryPolicy.UPDATE_POLICY_ALWAYS.equals( policy ) )
+            {
+                return 0;
+            }
+            else if ( policy != null && policy.startsWith( RepositoryPolicy.UPDATE_POLICY_INTERVAL ) )
+            {
+                String s = policy.substring( RepositoryPolicy.UPDATE_POLICY_INTERVAL.length() + 1 );
+                return Integer.valueOf( s );
+            }
+            else
+            {
+                // assume "never"
+                return Integer.MAX_VALUE;
+            }
+        }
+
+        public boolean isUpdatedRequired( RepositorySystemSession session, long lastModified, String policy )
+        {
+            return false;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcherTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcherTest.java
new file mode 100644
index 0000000..25e8a87
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositoryEventDispatcherTest.java
@@ -0,0 +1,90 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Locale;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultRepositoryEventDispatcherTest
+{
+
+    @Test
+    public void testDispatchHandlesAllEventTypes()
+        throws Exception
+    {
+        DefaultRepositoryEventDispatcher dispatcher = new DefaultRepositoryEventDispatcher();
+
+        ListenerHandler handler = new ListenerHandler();
+
+        RepositoryListener listener =
+            (RepositoryListener) Proxy.newProxyInstance( getClass().getClassLoader(),
+                                                         new Class<?>[] { RepositoryListener.class }, handler );
+
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        session.setRepositoryListener( listener );
+
+        for ( RepositoryEvent.EventType type : RepositoryEvent.EventType.values() )
+        {
+            RepositoryEvent event = new RepositoryEvent.Builder( session, type ).build();
+
+            handler.methodName = null;
+
+            dispatcher.dispatch( event );
+
+            assertNotNull( "not handled: " + type, handler.methodName );
+
+            assertEquals( "badly handled: " + type, type.name().replace( "_", "" ).toLowerCase( Locale.ENGLISH ),
+                          handler.methodName.toLowerCase( Locale.ENGLISH ) );
+        }
+    }
+
+    static class ListenerHandler
+        implements InvocationHandler
+    {
+
+        public String methodName;
+
+        public Object invoke( Object proxy, Method method, Object[] args )
+            throws Throwable
+        {
+            if ( args.length == 1 && args[0] instanceof RepositoryEvent )
+            {
+                methodName = method.getName();
+            }
+
+            return null;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
new file mode 100644
index 0000000..65acfdb
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
+import org.eclipse.aether.util.repository.DefaultMirrorSelector;
+import org.eclipse.aether.util.repository.DefaultProxySelector;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultRepositorySystemTest
+{
+
+    private DefaultRepositorySystem system;
+
+    private DefaultRepositorySystemSession session;
+
+    @Before
+    public void init()
+    {
+        DefaultRemoteRepositoryManager remoteRepoManager = new DefaultRemoteRepositoryManager();
+        system = new DefaultRepositorySystem();
+        system.setRemoteRepositoryManager( remoteRepoManager );
+        session = TestUtils.newSession();
+    }
+
+    @Test
+    public void testNewResolutionRepositories()
+    {
+        Proxy proxy = new Proxy( "http", "localhost", 8080 );
+        DefaultProxySelector proxySelector = new DefaultProxySelector();
+        proxySelector.add( proxy, null );
+        session.setProxySelector( proxySelector );
+
+        Authentication auth = new AuthenticationBuilder().addUsername( "user" ).build();
+        DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
+        authSelector.add( "mirror", auth );
+        authSelector.add( "test-2", auth );
+        session.setAuthenticationSelector( authSelector );
+
+        DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
+        mirrorSelector.add( "mirror", "http:void", "default", false, "test-1", null );
+        session.setMirrorSelector( mirrorSelector );
+
+        RemoteRepository rawRepo1 = new RemoteRepository.Builder( "test-1", "default", "http://void" ).build();
+        RemoteRepository rawRepo2 = new RemoteRepository.Builder( "test-2", "default", "http://null" ).build();
+        List<RemoteRepository> resolveRepos =
+            system.newResolutionRepositories( session, Arrays.asList( rawRepo1, rawRepo2 ) );
+        assertNotNull( resolveRepos );
+        assertEquals( 2, resolveRepos.size() );
+        RemoteRepository resolveRepo = resolveRepos.get( 0 );
+        assertNotNull( resolveRepo );
+        assertEquals( "mirror", resolveRepo.getId() );
+        assertSame( proxy, resolveRepo.getProxy() );
+        assertSame( auth, resolveRepo.getAuthentication() );
+        resolveRepo = resolveRepos.get( 1 );
+        assertNotNull( resolveRepo );
+        assertEquals( "test-2", resolveRepo.getId() );
+        assertSame( proxy, resolveRepo.getProxy() );
+        assertSame( auth, resolveRepo.getAuthentication() );
+    }
+
+    @Test
+    public void testNewDeploymentRepository()
+    {
+        Proxy proxy = new Proxy( "http", "localhost", 8080 );
+        DefaultProxySelector proxySelector = new DefaultProxySelector();
+        proxySelector.add( proxy, null );
+        session.setProxySelector( proxySelector );
+
+        Authentication auth = new AuthenticationBuilder().addUsername( "user" ).build();
+        DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
+        authSelector.add( "test", auth );
+        session.setAuthenticationSelector( authSelector );
+
+        DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
+        mirrorSelector.add( "mirror", "file:void", "default", false, "*", null );
+        session.setMirrorSelector( mirrorSelector );
+
+        RemoteRepository rawRepo = new RemoteRepository.Builder( "test", "default", "http://void" ).build();
+        RemoteRepository deployRepo = system.newDeploymentRepository( session, rawRepo );
+        assertNotNull( deployRepo );
+        assertSame( proxy, deployRepo.getProxy() );
+        assertSame( auth, deployRepo.getAuthentication() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
new file mode 100644
index 0000000..9cb299c
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
@@ -0,0 +1,816 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.internal.impl.DefaultUpdateCheckManager;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.util.repository.SimpleResolutionErrorPolicy;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultUpdateCheckManagerTest
+{
+
+    private static final long HOUR = 60L * 60L * 1000L;
+
+    private DefaultUpdateCheckManager manager;
+
+    private DefaultRepositorySystemSession session;
+
+    private Metadata metadata;
+
+    private RemoteRepository repository;
+
+    private Artifact artifact;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        File dir = TestFileUtils.createTempFile( "" );
+        TestFileUtils.deleteFile( dir );
+
+        File metadataFile = new File( dir, "metadata.txt" );
+        TestFileUtils.writeString( metadataFile, "metadata" );
+        File artifactFile = new File( dir, "artifact.txt" );
+        TestFileUtils.writeString( artifactFile, "artifact" );
+
+        session = TestUtils.newSession();
+        repository =
+            new RemoteRepository.Builder( "id", "default", TestFileUtils.createTempDir().toURI().toURL().toString() ).build();
+        manager = new DefaultUpdateCheckManager().setUpdatePolicyAnalyzer( new DefaultUpdatePolicyAnalyzer() );
+        metadata =
+            new DefaultMetadata( "gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT,
+                                 metadataFile );
+        artifact = new DefaultArtifact( "gid", "aid", "", "ext", "ver" ).setFile( artifactFile );
+    }
+
+    @After
+    public void teardown()
+        throws Exception
+    {
+        new File( metadata.getFile().getParent(), "resolver-status.properties" ).delete();
+        new File( artifact.getFile().getPath() + ".lastUpdated" ).delete();
+        metadata.getFile().delete();
+        artifact.getFile().delete();
+        TestFileUtils.deleteFile( new File( new URI( repository.getUrl() ) ) );
+    }
+
+    static void resetSessionData( RepositorySystemSession session )
+    {
+        session.getData().set( "updateCheckManager.checks", null );
+    }
+
+    private UpdateCheck<Metadata, MetadataTransferException> newMetadataCheck()
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = new UpdateCheck<Metadata, MetadataTransferException>();
+        check.setItem( metadata );
+        check.setFile( metadata.getFile() );
+        check.setRepository( repository );
+        check.setAuthoritativeRepository( repository );
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":10" );
+        return check;
+    }
+
+    private UpdateCheck<Artifact, ArtifactTransferException> newArtifactCheck()
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = new UpdateCheck<Artifact, ArtifactTransferException>();
+        check.setItem( artifact );
+        check.setFile( artifact.getFile() );
+        check.setRepository( repository );
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":10" );
+        return check;
+    }
+
+    @Test( expected = Exception.class )
+    public void testCheckMetadataFailOnNoFile()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setItem( metadata.setFile( null ) );
+        check.setFile( null );
+
+        manager.checkMetadata( session, check );
+    }
+
+    @Test
+    public void testCheckMetadataUpdatePolicyRequired()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+
+        Calendar cal = Calendar.getInstance();
+        cal.add( Calendar.DATE, -1 );
+        check.setLocalLastUpdated( cal.getTimeInMillis() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        manager.checkMetadata( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkMetadata( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60" );
+        manager.checkMetadata( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataUpdatePolicyNotRequired()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+
+        check.setLocalLastUpdated( System.currentTimeMillis() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        manager.checkMetadata( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkMetadata( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":61" );
+        manager.checkMetadata( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( "no particular policy" );
+        manager.checkMetadata( session, check );
+        assertFalse( check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadata()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+
+        // existing file, never checked before
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // just checked
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60" );
+
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+
+        // no local file
+        check.getFile().delete();
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+        // (! file.exists && ! repoKey) -> no timestamp
+    }
+
+    @Test
+    public void testCheckMetadataNoLocalFile()
+        throws Exception
+    {
+        metadata.getFile().delete();
+
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+
+        long lastUpdate = new Date().getTime() - HOUR;
+        check.setLocalLastUpdated( lastUpdate );
+
+        // ! file.exists && updateRequired -> check in remote repo
+        check.setLocalLastUpdated( lastUpdate );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataNotFoundInRepoCachingEnabled()
+        throws Exception
+    {
+        metadata.getFile().delete();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+
+        check.setException( new MetadataNotFoundException( metadata, repository, "" ) );
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired -> artifact not found in remote repo
+        check = newMetadataCheck().setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+        assertTrue( check.getException() instanceof MetadataNotFoundException );
+        assertTrue( check.getException().isFromCache() );
+    }
+
+    @Test
+    public void testCheckMetadataNotFoundInRepoCachingDisabled()
+        throws Exception
+    {
+        metadata.getFile().delete();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, false ) );
+
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+
+        check.setException( new MetadataNotFoundException( metadata, repository, "" ) );
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && updateRequired -> check in remote repo
+        check = newMetadataCheck().setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+        assertNull( check.getException() );
+    }
+
+    @Test
+    public void testCheckMetadataErrorFromRepoCachingEnabled()
+        throws Exception
+    {
+        metadata.getFile().delete();
+
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+
+        check.setException( new MetadataTransferException( metadata, repository, "some error" ) );
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching
+        check = newMetadataCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, true ) );
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+        assertTrue( check.getException() instanceof MetadataTransferException );
+        assertTrue( String.valueOf( check.getException() ), check.getException().getMessage().contains( "some error" ) );
+        assertTrue( check.getException().isFromCache() );
+    }
+
+    @Test
+    public void testCheckMetadataErrorFromRepoCachingDisabled()
+        throws Exception
+    {
+        metadata.getFile().delete();
+
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+
+        check.setException( new MetadataTransferException( metadata, repository, "some error" ) );
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching
+        check = newMetadataCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, false ) );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+        assertNull( check.getException() );
+    }
+
+    @Test
+    public void testCheckMetadataAtMostOnceDuringSessionEvenIfUpdatePolicyAlways()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+
+        // first check
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        manager.touchMetadata( session, check );
+
+        // second check in same session
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataSessionStateModes()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        manager.touchMetadata( session, check );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "bypass" );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        resetSessionData( session );
+        manager.touchMetadata( session, check );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "true" );
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "false" );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataAtMostOnceDuringSessionEvenIfUpdatePolicyAlways_InvalidFile()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        check.setFileValid( false );
+
+        // first check
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // first touch, without exception
+        manager.touchMetadata( session, check );
+
+        // another check in same session
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // another touch, with exception
+        check.setException( new MetadataNotFoundException( check.getItem(), check.getRepository() ) );
+        manager.touchMetadata( session, check );
+
+        // another check in same session
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataAtMostOnceDuringSessionEvenIfUpdatePolicyAlways_DifferentRepoIdSameUrl()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        check.setFileValid( false );
+
+        // first check
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+
+        manager.touchMetadata( session, check );
+
+        // second check in same session but for repo with different id
+        check.setRepository( new RemoteRepository.Builder( check.getRepository() ).setId( "check" ).build() );
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataWhenLocallyMissingEvenIfUpdatePolicyIsNever()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        check.getFile().delete();
+        assertEquals( check.getFile().getAbsolutePath(), false, check.getFile().exists() );
+
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataWhenLocallyPresentButInvalidEvenIfUpdatePolicyIsNever()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        check.setFileValid( false );
+
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataWhenLocallyDeletedEvenIfTimestampUpToDate()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.touchMetadata( session, check );
+        resetSessionData( session );
+
+        check.getFile().delete();
+        assertEquals( check.getFile().getAbsolutePath(), false, check.getFile().exists() );
+
+        manager.checkMetadata( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckMetadataNotWhenUpdatePolicyIsNeverAndTimestampIsUnavailable()
+        throws Exception
+    {
+        UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.checkMetadata( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+    @Test( expected = NullPointerException.class )
+    public void testCheckArtifactFailOnNoFile()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setItem( artifact.setFile( null ) );
+        check.setFile( null );
+
+        manager.checkArtifact( session, check );
+        assertNotNull( check.getException() );
+    }
+
+    @Test
+    public void testCheckArtifactUpdatePolicyRequired()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setItem( artifact );
+        check.setFile( artifact.getFile() );
+
+        Calendar cal = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) );
+        cal.add( Calendar.DATE, -1 );
+        long lastUpdate = cal.getTimeInMillis();
+        artifact.getFile().setLastModified( lastUpdate );
+        check.setLocalLastUpdated( lastUpdate );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        manager.checkArtifact( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkArtifact( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60" );
+        manager.checkArtifact( session, check );
+        assertNull( check.getException() );
+        assertTrue( check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactUpdatePolicyNotRequired()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setItem( artifact );
+        check.setFile( artifact.getFile() );
+
+        Calendar cal = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) );
+        cal.add( Calendar.HOUR_OF_DAY, -1 );
+        check.setLocalLastUpdated( cal.getTimeInMillis() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        manager.checkArtifact( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkArtifact( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":61" );
+        manager.checkArtifact( session, check );
+        assertFalse( check.isRequired() );
+
+        check.setPolicy( "no particular policy" );
+        manager.checkArtifact( session, check );
+        assertFalse( check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifact()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        long fifteenMinutes = new Date().getTime() - ( 15L * 60L * 1000L );
+        check.getFile().setLastModified( fifteenMinutes );
+        // time is truncated on setLastModfied
+        fifteenMinutes = check.getFile().lastModified();
+
+        // never checked before
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // just checked
+        check.setLocalLastUpdated( 0L );
+        long lastUpdate = new Date().getTime();
+        check.getFile().setLastModified( lastUpdate );
+        lastUpdate = check.getFile().lastModified();
+
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+
+        // no local file, no repo timestamp
+        check.setLocalLastUpdated( 0L );
+        check.getFile().delete();
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactNoLocalFile()
+        throws Exception
+    {
+        artifact.getFile().delete();
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+
+        long lastUpdate = new Date().getTime() - HOUR;
+
+        // ! file.exists && updateRequired -> check in remote repo
+        check.setLocalLastUpdated( lastUpdate );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactNotFoundInRepoCachingEnabled()
+        throws Exception
+    {
+        artifact.getFile().delete();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setException( new ArtifactNotFoundException( artifact, repository ) );
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired -> artifact not found in remote repo
+        check = newArtifactCheck().setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+        assertTrue( check.getException() instanceof ArtifactNotFoundException );
+        assertTrue( check.getException().isFromCache() );
+    }
+
+    @Test
+    public void testCheckArtifactNotFoundInRepoCachingDisabled()
+        throws Exception
+    {
+        artifact.getFile().delete();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, false ) );
+
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setException( new ArtifactNotFoundException( artifact, repository ) );
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && updateRequired -> check in remote repo
+        check = newArtifactCheck().setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+        assertNull( check.getException() );
+    }
+
+    @Test
+    public void testCheckArtifactErrorFromRepoCachingEnabled()
+        throws Exception
+    {
+        artifact.getFile().delete();
+
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        check.setException( new ArtifactTransferException( artifact, repository, "some error" ) );
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching
+        check = newArtifactCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, true ) );
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+        assertTrue( check.getException() instanceof ArtifactTransferException );
+        assertTrue( check.getException().isFromCache() );
+    }
+
+    @Test
+    public void testCheckArtifactErrorFromRepoCachingDisabled()
+        throws Exception
+    {
+        artifact.getFile().delete();
+
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_DAILY );
+        check.setException( new ArtifactTransferException( artifact, repository, "some error" ) );
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching
+        check = newArtifactCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( false, false ) );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+        assertNull( check.getException() );
+    }
+
+    @Test
+    public void testCheckArtifactAtMostOnceDuringSessionEvenIfUpdatePolicyAlways()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+
+        // first check
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        manager.touchArtifact( session, check );
+
+        // second check in same session
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactSessionStateModes()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        manager.touchArtifact( session, check );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "bypass" );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        resetSessionData( session );
+        manager.touchArtifact( session, check );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "true" );
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+
+        session.setConfigProperty( DefaultUpdateCheckManager.CONFIG_PROP_SESSION_STATE, "false" );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactAtMostOnceDuringSessionEvenIfUpdatePolicyAlways_InvalidFile()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+        check.setFileValid( false );
+
+        // first check
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // first touch, without exception
+        manager.touchArtifact( session, check );
+
+        // another check in same session
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        // another touch, with exception
+        check.setException( new ArtifactNotFoundException( check.getItem(), check.getRepository() ) );
+        manager.touchArtifact( session, check );
+
+        // another check in same session
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactAtMostOnceDuringSessionEvenIfUpdatePolicyAlways_DifferentRepoIdSameUrl()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_ALWAYS );
+
+        // first check
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+
+        manager.touchArtifact( session, check );
+
+        // second check in same session but for repo with different id
+        check.setRepository( new RemoteRepository.Builder( check.getRepository() ).setId( "check" ).build() );
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactWhenLocallyMissingEvenIfUpdatePolicyIsNever()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        check.getFile().delete();
+        assertEquals( check.getFile().getAbsolutePath(), false, check.getFile().exists() );
+
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactWhenLocallyPresentButInvalidEvenIfUpdatePolicyIsNever()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        check.setFileValid( false );
+
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactWhenLocallyDeletedEvenIfTimestampUpToDate()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.touchArtifact( session, check );
+        resetSessionData( session );
+
+        check.getFile().delete();
+        assertEquals( check.getFile().getAbsolutePath(), false, check.getFile().exists() );
+
+        manager.checkArtifact( session, check );
+        assertEquals( true, check.isRequired() );
+    }
+
+    @Test
+    public void testCheckArtifactNotWhenUpdatePolicyIsNeverAndTimestampIsUnavailable()
+        throws Exception
+    {
+        UpdateCheck<Artifact, ArtifactTransferException> check = newArtifactCheck();
+        check.setPolicy( RepositoryPolicy.UPDATE_POLICY_NEVER );
+        session.setResolutionErrorPolicy( new SimpleResolutionErrorPolicy( true, false ) );
+
+        manager.checkArtifact( session, check );
+        assertEquals( false, check.isRequired() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzerTest.java
new file mode 100644
index 0000000..31bcbaa
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdatePolicyAnalyzerTest.java
@@ -0,0 +1,130 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.eclipse.aether.repository.RepositoryPolicy.*;
+import static org.junit.Assert.*;
+
+import java.util.Calendar;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultUpdatePolicyAnalyzerTest
+{
+
+    private DefaultUpdatePolicyAnalyzer analyzer;
+
+    private DefaultRepositorySystemSession session;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        analyzer = new DefaultUpdatePolicyAnalyzer();
+        session = TestUtils.newSession();
+    }
+
+    private long now()
+    {
+        return System.currentTimeMillis();
+    }
+
+    @Test
+    public void testIsUpdateRequired_PolicyNever()
+        throws Exception
+    {
+        String policy = RepositoryPolicy.UPDATE_POLICY_NEVER;
+        assertEquals( false, analyzer.isUpdatedRequired( session, Long.MIN_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, Long.MAX_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, 0, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, 1, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, now() - 604800000, policy ) );
+    }
+
+    @Test
+    public void testIsUpdateRequired_PolicyAlways()
+        throws Exception
+    {
+        String policy = RepositoryPolicy.UPDATE_POLICY_ALWAYS;
+        assertEquals( true, analyzer.isUpdatedRequired( session, Long.MIN_VALUE, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, Long.MAX_VALUE, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, 0, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, 1, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, now() - 1000, policy ) );
+    }
+
+    @Test
+    public void testIsUpdateRequired_PolicyDaily()
+        throws Exception
+    {
+        Calendar cal = Calendar.getInstance();
+        cal.set( Calendar.HOUR_OF_DAY, 0 );
+        cal.set( Calendar.MINUTE, 0 );
+        cal.set( Calendar.SECOND, 0 );
+        cal.set( Calendar.MILLISECOND, 0 );
+        long localMidnight = cal.getTimeInMillis();
+
+        String policy = RepositoryPolicy.UPDATE_POLICY_DAILY;
+        assertEquals( true, analyzer.isUpdatedRequired( session, Long.MIN_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, Long.MAX_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, localMidnight + 0, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, localMidnight + 1, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, localMidnight - 1, policy ) );
+    }
+
+    @Test
+    public void testIsUpdateRequired_PolicyInterval()
+        throws Exception
+    {
+        String policy = RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":5";
+        assertEquals( true, analyzer.isUpdatedRequired( session, Long.MIN_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, Long.MAX_VALUE, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, now(), policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, now() - 5 - 1, policy ) );
+        assertEquals( false, analyzer.isUpdatedRequired( session, now() - 1000 * 5 - 1, policy ) );
+        assertEquals( true, analyzer.isUpdatedRequired( session, now() - 1000 * 60 * 5 - 1, policy ) );
+
+        policy = RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":invalid";
+        assertEquals( false, analyzer.isUpdatedRequired( session, now(), policy ) );
+    }
+
+    @Test
+    public void testEffectivePolicy()
+    {
+        assertEquals( UPDATE_POLICY_ALWAYS,
+                      analyzer.getEffectiveUpdatePolicy( session, UPDATE_POLICY_ALWAYS, UPDATE_POLICY_DAILY ) );
+        assertEquals( UPDATE_POLICY_ALWAYS,
+                      analyzer.getEffectiveUpdatePolicy( session, UPDATE_POLICY_ALWAYS, UPDATE_POLICY_NEVER ) );
+        assertEquals( UPDATE_POLICY_DAILY,
+                      analyzer.getEffectiveUpdatePolicy( session, UPDATE_POLICY_DAILY, UPDATE_POLICY_NEVER ) );
+        assertEquals( UPDATE_POLICY_INTERVAL + ":60",
+                      analyzer.getEffectiveUpdatePolicy( session, UPDATE_POLICY_DAILY, UPDATE_POLICY_INTERVAL + ":60" ) );
+        assertEquals( UPDATE_POLICY_INTERVAL + ":60",
+                      analyzer.getEffectiveUpdatePolicy( session, UPDATE_POLICY_INTERVAL + ":100",
+                                                         UPDATE_POLICY_INTERVAL + ":60" ) );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DependencyGraphDumper.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DependencyGraphDumper.java
new file mode 100644
index 0000000..39bc1ed
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DependencyGraphDumper.java
@@ -0,0 +1,222 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A helper to visualize dependency graphs.
+ */
+public class DependencyGraphDumper
+{
+
+    public static void dump( PrintWriter writer, DependencyNode node )
+    {
+        Context context = new Context();
+        dump( context, node, 0, true );
+
+        LinkedList<Indent> indents = new LinkedList<Indent>();
+        for ( Line line : context.lines )
+        {
+            if ( line.depth > indents.size() )
+            {
+                if ( !indents.isEmpty() )
+                {
+                    if ( indents.getLast() == Indent.CHILD )
+                    {
+                        indents.removeLast();
+                        indents.addLast( Indent.CHILDREN );
+                    }
+                    else if ( indents.getLast() == Indent.LAST_CHILD )
+                    {
+                        indents.removeLast();
+                        indents.addLast( Indent.NO_CHILDREN );
+                    }
+                }
+                indents.addLast( line.last ? Indent.LAST_CHILD : Indent.CHILD );
+            }
+            else if ( line.depth < indents.size() )
+            {
+                while ( line.depth <= indents.size() )
+                {
+                    indents.removeLast();
+                }
+                indents.addLast( line.last ? Indent.LAST_CHILD : Indent.CHILD );
+            }
+            else if ( line.last && !indents.isEmpty() )
+            {
+                indents.removeLast();
+                indents.addLast( Indent.LAST_CHILD );
+            }
+
+            for ( Indent indent : indents )
+            {
+                writer.print( indent );
+            }
+
+            line.print( writer );
+        }
+
+        writer.flush();
+    }
+
+    private static void dump( Context context, DependencyNode node, int depth, boolean last )
+    {
+        Line line = context.nodes.get( node );
+        if ( line != null )
+        {
+            if ( line.id <= 0 )
+            {
+                line.id = ++context.ids;
+            }
+            context.lines.add( new Line( null, line.id, depth, last ) );
+            return;
+        }
+
+        Dependency dependency = node.getDependency();
+
+        if ( dependency == null )
+        {
+            line = new Line( null, 0, depth, last );
+        }
+        else
+        {
+            line = new Line( dependency, 0, depth, last );
+        }
+
+        context.lines.add( line );
+
+        context.nodes.put( node, line );
+
+        depth++;
+
+        for ( Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); )
+        {
+            DependencyNode child = it.next();
+            dump( context, child, depth, !it.hasNext() );
+        }
+    }
+
+    static enum Indent
+    {
+
+        NO_CHILDREN( "   " ),
+
+        CHILDREN( "|  " ),
+
+        CHILD( "+- " ),
+
+        LAST_CHILD( "\\- " );
+
+        private final String chars;
+
+        Indent( String chars )
+        {
+            this.chars = chars;
+        }
+
+        @Override
+        public String toString()
+        {
+            return chars;
+        }
+
+    }
+
+    static class Context
+    {
+
+        int ids;
+
+        List<Line> lines;
+
+        Map<DependencyNode, Line> nodes;
+
+        Context()
+        {
+            this.lines = new ArrayList<Line>();
+            this.nodes = new IdentityHashMap<DependencyNode, Line>( 1024 );
+        }
+
+    }
+
+    static class Line
+    {
+
+        Dependency dependency;
+
+        int id;
+
+        int depth;
+
+        boolean last;
+
+        Line( Dependency dependency, int id, int depth, boolean last )
+        {
+            this.dependency = dependency;
+            this.id = id;
+            this.depth = depth;
+            this.last = last;
+        }
+
+        void print( PrintWriter writer )
+        {
+            if ( dependency == null )
+            {
+                if ( id > 0 )
+                {
+                    writer.print( "^" );
+                    writer.print( id );
+                }
+                else
+                {
+                    writer.print( "(null)" );
+                }
+            }
+            else
+            {
+                if ( id > 0 )
+                {
+                    writer.print( "(" );
+                    writer.print( id );
+                    writer.print( ")" );
+                }
+                writer.print( dependency.getArtifact() );
+                if ( dependency.getScope().length() > 0 )
+                {
+                    writer.print( ":" );
+                    writer.print( dependency.getScope() );
+                }
+            }
+            writer.println();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
new file mode 100644
index 0000000..32a4222
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
@@ -0,0 +1,343 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.metadata.Metadata.Nature;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class EnhancedLocalRepositoryManagerTest
+{
+
+    private Artifact artifact;
+
+    private Artifact snapshot;
+
+    private File basedir;
+
+    private EnhancedLocalRepositoryManager manager;
+
+    private File artifactFile;
+
+    private RemoteRepository repository;
+
+    private String testContext = "project/compile";
+
+    private RepositorySystemSession session;
+
+    private Metadata metadata;
+
+    private Metadata noVerMetadata;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        String url = TestFileUtils.createTempDir( "enhanced-remote-repo" ).toURI().toURL().toString();
+        repository =
+            new RemoteRepository.Builder( "enhanced-remote-repo", "default", url ).setRepositoryManager( true ).build();
+
+        artifact =
+            new DefaultArtifact( "gid", "aid", "", "jar", "1-test", Collections.<String, String> emptyMap(),
+                                 TestFileUtils.createTempFile( "artifact" ) );
+
+        snapshot =
+            new DefaultArtifact( "gid", "aid", "", "jar", "1.0-20120710.231549-9",
+                                 Collections.<String, String> emptyMap(), TestFileUtils.createTempFile( "artifact" ) );
+
+        metadata =
+            new DefaultMetadata( "gid", "aid", "1-test", "maven-metadata.xml", Nature.RELEASE,
+                                 TestFileUtils.createTempFile( "metadata" ) );
+
+        noVerMetadata =
+            new DefaultMetadata( "gid", "aid", null, "maven-metadata.xml", Nature.RELEASE,
+                                 TestFileUtils.createTempFile( "metadata" ) );
+
+        basedir = TestFileUtils.createTempDir( "enhanced-repo" );
+        session = TestUtils.newSession();
+        manager = new EnhancedLocalRepositoryManager( basedir, session );
+
+        artifactFile = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @After
+    public void tearDown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( basedir );
+        TestFileUtils.deleteFile( new File( new URI( repository.getUrl() ) ) );
+
+        session = null;
+        manager = null;
+        repository = null;
+        artifact = null;
+    }
+
+    private long addLocalArtifact( Artifact artifact )
+        throws IOException
+    {
+        manager.add( session, new LocalArtifactRegistration( artifact ) );
+        String path = manager.getPathForLocalArtifact( artifact );
+
+        return copy( artifact, path );
+    }
+
+    private long addRemoteArtifact( Artifact artifact )
+        throws IOException
+    {
+        Collection<String> contexts = Arrays.asList( testContext );
+        manager.add( session, new LocalArtifactRegistration( artifact, repository, contexts ) );
+        String path = manager.getPathForRemoteArtifact( artifact, repository, testContext );
+        return copy( artifact, path );
+    }
+
+    private long copy( Metadata metadata, String path )
+        throws IOException
+    {
+        if ( metadata.getFile() == null )
+        {
+            return -1L;
+        }
+        return TestFileUtils.copyFile( metadata.getFile(), new File( basedir, path ) );
+    }
+
+    private long copy( Artifact artifact, String path )
+        throws IOException
+    {
+        if ( artifact.getFile() == null )
+        {
+            return -1L;
+        }
+        File artifactFile = new File( basedir, path );
+        return TestFileUtils.copyFile( artifact.getFile(), artifactFile );
+    }
+
+    @Test
+    public void testGetPathForLocalArtifact()
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @Test
+    public void testGetPathForRemoteArtifact()
+    {
+        RemoteRepository remoteRepo = new RemoteRepository.Builder( "repo", "default", "ram:/void" ).build();
+
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+    }
+
+    @Test
+    public void testFindLocalArtifact()
+        throws Exception
+    {
+        addLocalArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, null, null );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( null, result.getRepository() );
+
+        snapshot = snapshot.setVersion( snapshot.getBaseVersion() );
+        addLocalArtifact( snapshot );
+
+        request = new LocalArtifactRequest( snapshot, null, null );
+        result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( null, result.getRepository() );
+    }
+
+    @Test
+    public void testFindRemoteArtifact()
+        throws Exception
+    {
+        addRemoteArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( repository, result.getRepository() );
+
+        addRemoteArtifact( snapshot );
+
+        request = new LocalArtifactRequest( snapshot, Arrays.asList( repository ), testContext );
+        result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( repository, result.getRepository() );
+    }
+
+    @Test
+    public void testDoNotFindDifferentContext()
+        throws Exception
+    {
+        addRemoteArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), "different" );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testDoNotFindNullFile()
+        throws Exception
+    {
+        artifact = artifact.setFile( null );
+        addLocalArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testDoNotFindDeletedFile()
+        throws Exception
+    {
+        addLocalArtifact( artifact );
+        assertTrue( "could not delete artifact file", artifactFile.delete() );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testFindUntrackedFile()
+        throws Exception
+    {
+        copy( artifact, manager.getPathForLocalArtifact( artifact ) );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+    }
+
+    private long addMetadata( Metadata metadata, RemoteRepository repo )
+        throws IOException
+    {
+        String path;
+        if ( repo == null )
+        {
+            path = manager.getPathForLocalMetadata( metadata );
+        }
+        else
+        {
+            path = manager.getPathForRemoteMetadata( metadata, repo, testContext );
+        }
+        System.err.println( path );
+
+        return copy( metadata, path );
+    }
+
+    @Test
+    public void testFindLocalMetadata()
+        throws Exception
+    {
+        addMetadata( metadata, null );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( metadata, null, testContext );
+        LocalMetadataResult result = manager.find( session, request );
+
+        assertNotNull( result.getFile() );
+    }
+
+    @Test
+    public void testFindLocalMetadataNoVersion()
+        throws Exception
+    {
+        addMetadata( noVerMetadata, null );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( noVerMetadata, null, testContext );
+        LocalMetadataResult result = manager.find( session, request );
+
+        assertNotNull( result.getFile() );
+    }
+
+    @Test
+    public void testDoNotFindRemoteMetadataDifferentContext()
+        throws Exception
+    {
+        addMetadata( noVerMetadata, repository );
+        addMetadata( metadata, repository );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( noVerMetadata, repository, "different" );
+        LocalMetadataResult result = manager.find( session, request );
+        assertNull( result.getFile() );
+
+        request = new LocalMetadataRequest( metadata, repository, "different" );
+        result = manager.find( session, request );
+        assertNull( result.getFile() );
+    }
+
+    @Test
+    public void testFindArtifactUsesTimestampedVersion()
+        throws Exception
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        File file = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
+        TestFileUtils.writeString( file, "test" );
+        addLocalArtifact( artifact );
+
+        artifact = artifact.setVersion( "1.0-20110329.221805-4" );
+        LocalArtifactRequest request = new LocalArtifactRequest();
+        request.setArtifact( artifact );
+        LocalArtifactResult result = manager.find( session, request );
+        assertNull( result.toString(), result.getFile() );
+        assertFalse( result.toString(), result.isAvailable() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java
new file mode 100644
index 0000000..296f829
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java
@@ -0,0 +1,94 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.TransferResource;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FailChecksumPolicyTest
+{
+
+    private FailChecksumPolicy policy;
+
+    private ChecksumFailureException exception;
+
+    @Before
+    public void setup()
+    {
+        policy = new FailChecksumPolicy( null, new TransferResource( "null", "file:/dev/null", "file.txt", null, null ) );
+        exception = new ChecksumFailureException( "test" );
+    }
+
+    @Test
+    public void testOnTransferChecksumFailure()
+    {
+        assertFalse( policy.onTransferChecksumFailure( exception ) );
+    }
+
+    @Test
+    public void testOnChecksumMatch()
+    {
+        assertTrue( policy.onChecksumMatch( "SHA-1", 0 ) );
+        assertTrue( policy.onChecksumMatch( "SHA-1", ChecksumPolicy.KIND_UNOFFICIAL ) );
+    }
+
+    @Test
+    public void testOnChecksumMismatch()
+        throws Exception
+    {
+        try
+        {
+            policy.onChecksumMismatch( "SHA-1", 0, exception );
+            fail( "No exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertSame( exception, e );
+        }
+        policy.onChecksumMismatch( "SHA-1", ChecksumPolicy.KIND_UNOFFICIAL, exception );
+    }
+
+    @Test
+    public void testOnChecksumError()
+        throws Exception
+    {
+        policy.onChecksumError( "SHA-1", 0, exception );
+    }
+
+    @Test
+    public void testOnNoMoreChecksums()
+    {
+        try
+        {
+            policy.onNoMoreChecksums();
+            fail( "No exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertTrue( e.getMessage().contains( "no checksums available" ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/IniArtifactDescriptorReader.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/IniArtifactDescriptorReader.java
new file mode 100644
index 0000000..4ae2b9b
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/IniArtifactDescriptorReader.java
@@ -0,0 +1,39 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+
+/**
+ */
+public class IniArtifactDescriptorReader
+    extends org.eclipse.aether.internal.test.util.IniArtifactDescriptorReader
+    implements ArtifactDescriptorReader
+{
+
+    /**
+     * Use the given prefix to load the artifact descriptions.
+     */
+    public IniArtifactDescriptorReader( String prefix )
+    {
+        super( prefix );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactoryTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactoryTest.java
new file mode 100644
index 0000000..7411a1d
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/Maven2RepositoryLayoutFactoryTest.java
@@ -0,0 +1,241 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout.Checksum;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class Maven2RepositoryLayoutFactoryTest
+{
+
+    private DefaultRepositorySystemSession session;
+
+    private Maven2RepositoryLayoutFactory factory;
+
+    private RepositoryLayout layout;
+
+    private RemoteRepository newRepo( String type )
+    {
+        return new RemoteRepository.Builder( "test", type, "classpath:/nil" ).build();
+    }
+
+    private void assertChecksum( Checksum actual, String expectedUri, String expectedAlgo )
+    {
+        assertEquals( expectedUri, actual.getLocation().toString() );
+        assertEquals( expectedAlgo, actual.getAlgorithm() );
+    }
+
+    private void assertChecksums( List<Checksum> actual, String baseUri, String... algos )
+    {
+        assertEquals( algos.length, actual.size() );
+        for ( int i = 0; i < algos.length; i++ )
+        {
+            String uri = baseUri + '.' + algos[i].replace( "-", "" ).toLowerCase( Locale.ENGLISH );
+            assertChecksum( actual.get( i ), uri, algos[i] );
+        }
+    }
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        factory = new Maven2RepositoryLayoutFactory();
+        layout = factory.newInstance( session, newRepo( "default" ) );
+    }
+
+    @Test( expected = NoRepositoryLayoutException.class )
+    public void testBadLayout()
+        throws Exception
+    {
+        factory.newInstance( session, newRepo( "DEFAULT" ) );
+    }
+
+    @Test
+    public void testArtifactLocation_Release()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "ext", "1.0" );
+        URI uri = layout.getLocation( artifact, false );
+        assertEquals( "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext", uri.toString() );
+        uri = layout.getLocation( artifact, true );
+        assertEquals( "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext", uri.toString() );
+    }
+
+    @Test
+    public void testArtifactLocation_Snapshot()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "ext", "1.0-20110329.221805-4" );
+        URI uri = layout.getLocation( artifact, false );
+        assertEquals( "g/i/d/a-i.d/1.0-SNAPSHOT/a-i.d-1.0-20110329.221805-4-cls.ext", uri.toString() );
+        uri = layout.getLocation( artifact, true );
+        assertEquals( "g/i/d/a-i.d/1.0-SNAPSHOT/a-i.d-1.0-20110329.221805-4-cls.ext", uri.toString() );
+    }
+
+    @Test
+    public void testMetadataLocation_RootLevel()
+    {
+        DefaultMetadata metadata = new DefaultMetadata( "archetype-catalog.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, false );
+        assertEquals( "archetype-catalog.xml", uri.toString() );
+        uri = layout.getLocation( metadata, true );
+        assertEquals( "archetype-catalog.xml", uri.toString() );
+    }
+
+    @Test
+    public void testMetadataLocation_GroupLevel()
+    {
+        DefaultMetadata metadata =
+            new DefaultMetadata( "org.apache.maven.plugins", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, false );
+        assertEquals( "org/apache/maven/plugins/maven-metadata.xml", uri.toString() );
+        uri = layout.getLocation( metadata, true );
+        assertEquals( "org/apache/maven/plugins/maven-metadata.xml", uri.toString() );
+    }
+
+    @Test
+    public void testMetadataLocation_ArtifactLevel()
+    {
+        DefaultMetadata metadata =
+            new DefaultMetadata( "org.apache.maven.plugins", "maven-jar-plugin", "maven-metadata.xml",
+                                 Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, false );
+        assertEquals( "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml", uri.toString() );
+        uri = layout.getLocation( metadata, true );
+        assertEquals( "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml", uri.toString() );
+    }
+
+    @Test
+    public void testMetadataLocation_VersionLevel()
+    {
+        DefaultMetadata metadata =
+            new DefaultMetadata( "org.apache.maven.plugins", "maven-jar-plugin", "1.0-SNAPSHOT", "maven-metadata.xml",
+                                 Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, false );
+        assertEquals( "org/apache/maven/plugins/maven-jar-plugin/1.0-SNAPSHOT/maven-metadata.xml", uri.toString() );
+        uri = layout.getLocation( metadata, true );
+        assertEquals( "org/apache/maven/plugins/maven-jar-plugin/1.0-SNAPSHOT/maven-metadata.xml", uri.toString() );
+    }
+
+    @Test
+    public void testArtifactChecksums_Download()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "ext", "1.0" );
+        URI uri = layout.getLocation( artifact, false );
+        List<Checksum> checksums = layout.getChecksums( artifact, false, uri );
+        assertEquals( 2, checksums.size() );
+        assertChecksum( checksums.get( 0 ), "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext.sha1", "SHA-1" );
+        assertChecksum( checksums.get( 1 ), "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext.md5", "MD5" );
+    }
+
+    @Test
+    public void testArtifactChecksums_Upload()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "ext", "1.0" );
+        URI uri = layout.getLocation( artifact, true );
+        List<Checksum> checksums = layout.getChecksums( artifact, true, uri );
+        assertEquals( 2, checksums.size() );
+        assertChecksum( checksums.get( 0 ), "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext.sha1", "SHA-1" );
+        assertChecksum( checksums.get( 1 ), "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.ext.md5", "MD5" );
+    }
+
+    @Test
+    public void testMetadataChecksums_Download()
+    {
+        DefaultMetadata metadata =
+            new DefaultMetadata( "org.apache.maven.plugins", "maven-jar-plugin", "maven-metadata.xml",
+                                 Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, false );
+        List<Checksum> checksums = layout.getChecksums( metadata, false, uri );
+        assertEquals( 2, checksums.size() );
+        assertChecksum( checksums.get( 0 ), "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml.sha1",
+                        "SHA-1" );
+        assertChecksum( checksums.get( 1 ), "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml.md5", "MD5" );
+    }
+
+    @Test
+    public void testMetadataChecksums_Upload()
+    {
+        DefaultMetadata metadata =
+            new DefaultMetadata( "org.apache.maven.plugins", "maven-jar-plugin", "maven-metadata.xml",
+                                 Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        URI uri = layout.getLocation( metadata, true );
+        List<Checksum> checksums = layout.getChecksums( metadata, true, uri );
+        assertEquals( 2, checksums.size() );
+        assertChecksum( checksums.get( 0 ), "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml.sha1",
+                        "SHA-1" );
+        assertChecksum( checksums.get( 1 ), "org/apache/maven/plugins/maven-jar-plugin/maven-metadata.xml.md5", "MD5" );
+    }
+
+    @Test
+    public void testSignatureChecksums_Download()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "asc", "1.0" );
+        URI uri = layout.getLocation( artifact, false );
+        List<Checksum> checksums = layout.getChecksums( artifact, false, uri );
+        assertChecksums( checksums, "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.asc", "SHA-1", "MD5" );
+
+        artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "jar.asc", "1.0" );
+        uri = layout.getLocation( artifact, false );
+        checksums = layout.getChecksums( artifact, false, uri );
+        assertEquals( 0, checksums.size() );
+    }
+
+    @Test
+    public void testSignatureChecksums_Upload()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "asc", "1.0" );
+        URI uri = layout.getLocation( artifact, true );
+        List<Checksum> checksums = layout.getChecksums( artifact, true, uri );
+        assertChecksums( checksums, "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.asc", "SHA-1", "MD5" );
+
+        artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "jar.asc", "1.0" );
+        uri = layout.getLocation( artifact, true );
+        checksums = layout.getChecksums( artifact, true, uri );
+        assertEquals( 0, checksums.size() );
+    }
+
+    @Test
+    public void testSignatureChecksums_Force()
+        throws Exception
+    {
+        session.setConfigProperty( Maven2RepositoryLayoutFactory.CONFIG_PROP_SIGNATURE_CHECKSUMS, "true" );
+        layout = factory.newInstance( session, newRepo( "default" ) );
+        DefaultArtifact artifact = new DefaultArtifact( "g.i.d", "a-i.d", "cls", "jar.asc", "1.0" );
+        URI uri = layout.getLocation( artifact, true );
+        List<Checksum> checksums = layout.getChecksums( artifact, true, uri );
+        assertChecksums( checksums, "g/i/d/a-i.d/1.0/a-i.d-1.0-cls.jar.asc", "SHA-1", "MD5" );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentTest.java
new file mode 100644
index 0000000..989c1ad
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentTest.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class PrioritizedComponentTest
+{
+
+    @Test
+    public void testIsDisabled()
+    {
+        assertTrue( new PrioritizedComponent<String>( "", String.class, Float.NaN, 0 ).isDisabled() );
+        assertFalse( new PrioritizedComponent<String>( "", String.class, 0, 0 ).isDisabled() );
+        assertFalse( new PrioritizedComponent<String>( "", String.class, 1, 0 ).isDisabled() );
+        assertFalse( new PrioritizedComponent<String>( "", String.class, -1, 0 ).isDisabled() );
+    }
+
+    @Test
+    public void testCompareTo()
+    {
+        assertCompare( 0, Float.NaN, Float.NaN );
+        assertCompare( 0, 0, 0 );
+
+        assertCompare( 1, 0, 1 );
+        assertCompare( 1, 2, Float.POSITIVE_INFINITY );
+        assertCompare( 1, Float.NEGATIVE_INFINITY, -3 );
+
+        assertCompare( 1, Float.NaN, 0 );
+        assertCompare( 1, Float.NaN, -1 );
+        assertCompare( 1, Float.NaN, Float.NEGATIVE_INFINITY );
+        assertCompare( 1, Float.NaN, Float.POSITIVE_INFINITY );
+
+        assertCompare( -1, Float.NaN, 0, 1 );
+        assertCompare( -1, 10, 0, 1 );
+    }
+
+    private void assertCompare( int expected, float priority1, float priority2 )
+    {
+        PrioritizedComponent<?> one = new PrioritizedComponent<String>( "", String.class, priority1, 0 );
+        PrioritizedComponent<?> two = new PrioritizedComponent<String>( "", String.class, priority2, 0 );
+        assertEquals( expected, one.compareTo( two ) );
+        assertEquals( -expected, two.compareTo( one ) );
+    }
+
+    private void assertCompare( int expected, float priority, int index1, int index2 )
+    {
+        PrioritizedComponent<?> one = new PrioritizedComponent<String>( "", String.class, priority, index1 );
+        PrioritizedComponent<?> two = new PrioritizedComponent<String>( "", String.class, priority, index2 );
+        assertEquals( expected, one.compareTo( two ) );
+        assertEquals( -expected, two.compareTo( one ) );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentsTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentsTest.java
new file mode 100644
index 0000000..3f5a093
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/PrioritizedComponentsTest.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+
+import org.eclipse.aether.ConfigurationProperties;
+import org.junit.Test;
+
+public class PrioritizedComponentsTest
+{
+
+    @Test
+    public void testGetConfigKeys()
+    {
+        String[] keys =
+            { ConfigurationProperties.PREFIX_PRIORITY + "java.lang.String",
+                ConfigurationProperties.PREFIX_PRIORITY + "String" };
+        assertArrayEquals( keys, PrioritizedComponents.getConfigKeys( String.class ) );
+
+        keys =
+            new String[] { ConfigurationProperties.PREFIX_PRIORITY + "java.util.concurrent.ThreadFactory",
+                ConfigurationProperties.PREFIX_PRIORITY + "ThreadFactory",
+                ConfigurationProperties.PREFIX_PRIORITY + "Thread" };
+        assertArrayEquals( keys, PrioritizedComponents.getConfigKeys( ThreadFactory.class ) );
+    }
+
+    @Test
+    public void testAdd_PriorityOverride()
+    {
+        Exception comp1 = new IllegalArgumentException();
+        Exception comp2 = new NullPointerException();
+        Map<Object, Object> config = new HashMap<Object, Object>();
+        config.put( ConfigurationProperties.PREFIX_PRIORITY + comp1.getClass().getName(), 6 );
+        config.put( ConfigurationProperties.PREFIX_PRIORITY + comp2.getClass().getName(), 7 );
+        PrioritizedComponents<Exception> components = new PrioritizedComponents<Exception>( config );
+        components.add( comp1, 1 );
+        components.add( comp2, 0 );
+        List<PrioritizedComponent<Exception>> sorted = components.getEnabled();
+        assertEquals( 2, sorted.size() );
+        assertSame( comp2, sorted.get( 0 ).getComponent() );
+        assertEquals( 7, sorted.get( 0 ).getPriority(), 0.1f );
+        assertSame( comp1, sorted.get( 1 ).getComponent() );
+        assertEquals( 6, sorted.get( 1 ).getPriority(), 0.1f );
+    }
+
+    @Test
+    public void testAdd_ImplicitPriority()
+    {
+        Exception comp1 = new IllegalArgumentException();
+        Exception comp2 = new NullPointerException();
+        Map<Object, Object> config = new HashMap<Object, Object>();
+        config.put( ConfigurationProperties.IMPLICIT_PRIORITIES, true );
+        PrioritizedComponents<Exception> components = new PrioritizedComponents<Exception>( config );
+        components.add( comp1, 1 );
+        components.add( comp2, 2 );
+        List<PrioritizedComponent<Exception>> sorted = components.getEnabled();
+        assertEquals( 2, sorted.size() );
+        assertSame( comp1, sorted.get( 0 ).getComponent() );
+        assertSame( comp2, sorted.get( 1 ).getComponent() );
+    }
+
+    @Test
+    public void testAdd_Disabled()
+    {
+        Exception comp1 = new IllegalArgumentException();
+        Exception comp2 = new NullPointerException();
+        Map<Object, Object> config = new HashMap<Object, Object>();
+        PrioritizedComponents<Exception> components = new PrioritizedComponents<Exception>( config );
+
+        components.add( new UnsupportedOperationException(), Float.NaN );
+        List<PrioritizedComponent<Exception>> sorted = components.getEnabled();
+        assertEquals( 0, sorted.size() );
+
+        components.add( comp1, 1 );
+        sorted = components.getEnabled();
+        assertEquals( 1, sorted.size() );
+        assertSame( comp1, sorted.get( 0 ).getComponent() );
+
+        components.add( new Exception(), Float.NaN );
+        sorted = components.getEnabled();
+        assertEquals( 1, sorted.size() );
+        assertSame( comp1, sorted.get( 0 ).getComponent() );
+
+        components.add( comp2, 0 );
+        sorted = components.getEnabled();
+        assertEquals( 2, sorted.size() );
+        assertSame( comp1, sorted.get( 0 ).getComponent() );
+        assertSame( comp2, sorted.get( 1 ).getComponent() );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java
new file mode 100644
index 0000000..80a347a
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java
@@ -0,0 +1,298 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.ArtifactUpload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.MetadataUpload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.connector.Transfer;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.transfer.TransferResource;
+
+/**
+ * A repository connector recording all get/put-requests and faking the results.
+ */
+class RecordingRepositoryConnector
+    implements RepositoryConnector
+{
+
+    RepositorySystemSession session;
+
+    boolean fail;
+
+    private Artifact[] expectGet;
+
+    private Artifact[] expectPut;
+
+    private Metadata[] expectGetMD;
+
+    private Metadata[] expectPutMD;
+
+    private List<Artifact> actualGet = new ArrayList<Artifact>();
+
+    private List<Metadata> actualGetMD = new ArrayList<Metadata>();
+
+    private List<Artifact> actualPut = new ArrayList<Artifact>();
+
+    private List<Metadata> actualPutMD = new ArrayList<Metadata>();
+
+    public RecordingRepositoryConnector( RepositorySystemSession session, Artifact[] expectGet, Artifact[] expectPut,
+                                         Metadata[] expectGetMD, Metadata[] expectPutMD )
+    {
+        this.session = session;
+        this.expectGet = expectGet;
+        this.expectPut = expectPut;
+        this.expectGetMD = expectGetMD;
+        this.expectPutMD = expectPutMD;
+    }
+
+    public RecordingRepositoryConnector( RepositorySystemSession session )
+    {
+        this.session = session;
+    }
+
+    public RecordingRepositoryConnector()
+    {
+    }
+
+    public void get( Collection<? extends ArtifactDownload> artifactDownloads,
+                     Collection<? extends MetadataDownload> metadataDownloads )
+    {
+        try
+        {
+            if ( artifactDownloads != null )
+            {
+                for ( ArtifactDownload download : artifactDownloads )
+                {
+                    fireInitiated( download );
+                    Artifact artifact = download.getArtifact();
+                    this.actualGet.add( artifact );
+                    if ( fail )
+                    {
+                        download.setException( new ArtifactTransferException( artifact, null, "forced failure" ) );
+                    }
+                    else
+                    {
+                        TestFileUtils.writeString( download.getFile(), artifact.toString() );
+                    }
+                    fireDone( download );
+                }
+            }
+            if ( metadataDownloads != null )
+            {
+                for ( MetadataDownload download : metadataDownloads )
+                {
+                    fireInitiated( download );
+                    Metadata metadata = download.getMetadata();
+                    this.actualGetMD.add( metadata );
+                    if ( fail )
+                    {
+                        download.setException( new MetadataTransferException( metadata, null, "forced failure" ) );
+                    }
+                    else
+                    {
+                        TestFileUtils.writeString( download.getFile(), metadata.toString() );
+                    }
+                    fireDone( download );
+                }
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( e );
+        }
+    }
+
+    public void put( Collection<? extends ArtifactUpload> artifactUploads,
+                     Collection<? extends MetadataUpload> metadataUploads )
+    {
+        try
+        {
+            if ( artifactUploads != null )
+            {
+                for ( ArtifactUpload upload : artifactUploads )
+                {
+                    // mimic "real" connector
+                    fireInitiated( upload );
+                    if ( upload.getFile() == null )
+                    {
+                        upload.setException( new ArtifactTransferException( upload.getArtifact(), null, "no file" ) );
+                    }
+                    else if ( fail )
+                    {
+                        upload.setException( new ArtifactTransferException( upload.getArtifact(), null,
+                                                                            "forced failure" ) );
+                    }
+                    this.actualPut.add( upload.getArtifact() );
+                    fireDone( upload );
+                }
+            }
+            if ( metadataUploads != null )
+            {
+                for ( MetadataUpload upload : metadataUploads )
+                {
+                    // mimic "real" connector
+                    fireInitiated( upload );
+                    if ( upload.getFile() == null )
+                    {
+                        upload.setException( new MetadataTransferException( upload.getMetadata(), null, "no file" ) );
+                    }
+                    else if ( fail )
+                    {
+                        upload.setException( new MetadataTransferException( upload.getMetadata(), null,
+                                                                            "forced failure" ) );
+                    }
+                    this.actualPutMD.add( upload.getMetadata() );
+                    fireDone( upload );
+                }
+            }
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( e );
+        }
+    }
+
+    private void fireInitiated( Transfer transfer )
+        throws Exception
+    {
+        TransferListener listener = transfer.getListener();
+        if ( listener == null )
+        {
+            return;
+        }
+        TransferEvent.Builder event =
+            new TransferEvent.Builder( session, new TransferResource( null, null, null, null, transfer.getTrace() ) );
+        event.setType( TransferEvent.EventType.INITIATED );
+        listener.transferInitiated( event.build() );
+    }
+
+    private void fireDone( Transfer transfer )
+        throws Exception
+    {
+        TransferListener listener = transfer.getListener();
+        if ( listener == null )
+        {
+            return;
+        }
+        TransferEvent.Builder event =
+            new TransferEvent.Builder( session, new TransferResource( null, null, null, null, transfer.getTrace() ) );
+        event.setException( transfer.getException() );
+        if ( transfer.getException() != null )
+        {
+            listener.transferFailed( event.setType( TransferEvent.EventType.FAILED ).build() );
+        }
+        else
+        {
+            listener.transferSucceeded( event.setType( TransferEvent.EventType.SUCCEEDED ).build() );
+        }
+    }
+
+    public void close()
+    {
+    }
+
+    public void assertSeenExpected()
+    {
+        assertSeenExpected( actualGet, expectGet );
+        assertSeenExpected( actualGetMD, expectGetMD );
+        assertSeenExpected( actualPut, expectPut );
+        assertSeenExpected( actualPutMD, expectPutMD );
+    }
+
+    private void assertSeenExpected( List<? extends Object> actual, Object[] expected )
+    {
+        if ( expected == null )
+        {
+            expected = new Object[0];
+        }
+
+        assertEquals( "different number of expected and actual elements:\n", expected.length, actual.size() );
+        int idx = 0;
+        for ( Object actualObject : actual )
+        {
+            assertEquals( "seen object differs", expected[idx++], actualObject );
+        }
+    }
+
+    public List<Artifact> getActualArtifactGetRequests()
+    {
+        return actualGet;
+    }
+
+    public List<Metadata> getActualMetadataGetRequests()
+    {
+        return actualGetMD;
+    }
+
+    public List<Artifact> getActualArtifactPutRequests()
+    {
+        return actualPut;
+    }
+
+    public List<Metadata> getActualMetadataPutRequests()
+    {
+        return actualPutMD;
+    }
+
+    public void setExpectGet( Artifact... expectGet )
+    {
+        this.expectGet = expectGet;
+    }
+
+    public void setExpectPut( Artifact... expectPut )
+    {
+        this.expectPut = expectPut;
+    }
+
+    public void setExpectGet( Metadata... expectGetMD )
+    {
+        this.expectGetMD = expectGetMD;
+    }
+
+    public void setExpectPut( Metadata... expectPutMD )
+    {
+        this.expectPutMD = expectPutMD;
+    }
+
+    public void resetActual()
+    {
+        this.actualGet = new ArrayList<Artifact>();
+        this.actualGetMD = new ArrayList<Metadata>();
+        this.actualPut = new ArrayList<Artifact>();
+        this.actualPutMD = new ArrayList<Metadata>();
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryListener.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryListener.java
new file mode 100644
index 0000000..a6f91f1
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryListener.java
@@ -0,0 +1,143 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryListener;
+
+/**
+ * Collects observed repository events for later inspection.
+ */
+class RecordingRepositoryListener
+    implements RepositoryListener
+{
+
+    private List<RepositoryEvent> events = Collections.synchronizedList( new ArrayList<RepositoryEvent>() );
+
+    public List<RepositoryEvent> getEvents()
+    {
+        return events;
+    }
+
+    public void clear()
+    {
+        events.clear();
+    }
+
+    public void artifactDescriptorInvalid( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactDescriptorMissing( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataInvalid( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactResolving( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactResolved( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactDownloading( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactDownloaded( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataDownloaded( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataDownloading( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataResolving( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataResolved( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactInstalling( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactInstalled( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataInstalling( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataInstalled( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactDeploying( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void artifactDeployed( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataDeploying( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+    public void metadataDeployed( RepositoryEvent event )
+    {
+        events.add( event );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SafeTransferListenerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SafeTransferListenerTest.java
new file mode 100644
index 0000000..6d7a6fe
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SafeTransferListenerTest.java
@@ -0,0 +1,45 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.aether.transfer.TransferListener;
+import org.junit.Test;
+
+/**
+ */
+public class SafeTransferListenerTest
+{
+
+    @Test
+    public void testAllEventTypesHandled()
+        throws Exception
+    {
+        Class<?> type = SafeTransferListener.class;
+        for ( Method method : TransferListener.class.getMethods() )
+        {
+            assertNotNull( type.getDeclaredMethod( method.getName(), method.getParameterTypes() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
new file mode 100644
index 0000000..a301bd4
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
@@ -0,0 +1,118 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManager;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class SimpleLocalRepositoryManagerTest
+{
+
+    private File basedir;
+
+    private SimpleLocalRepositoryManager manager;
+
+    private RepositorySystemSession session;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        basedir = TestFileUtils.createTempDir( "simple-repo" );
+        manager = new SimpleLocalRepositoryManager( basedir );
+        session = TestUtils.newSession();
+    }
+
+    @After
+    public void tearDown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( basedir );
+        manager = null;
+        session = null;
+    }
+
+    @Test
+    public void testGetPathForLocalArtifact()
+        throws Exception
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+
+        artifact = new DefaultArtifact( "g.i.d", "a.i.d", "", "", "1.0-SNAPSHOT" );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT", manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @Test
+    public void testGetPathForRemoteArtifact()
+        throws Exception
+    {
+        RemoteRepository remoteRepo = new RemoteRepository.Builder( "repo", "default", "ram:/void" ).build();
+
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+    }
+
+    @Test
+    public void testFindArtifactUsesTimestampedVersion()
+        throws Exception
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        File file = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
+        TestFileUtils.writeString( file, "test" );
+
+        artifact = artifact.setVersion( "1.0-20110329.221805-4" );
+        LocalArtifactRequest request = new LocalArtifactRequest();
+        request.setArtifact( artifact );
+        LocalArtifactResult result = manager.find( session, request );
+        assertNull( result.toString(), result.getFile() );
+        assertFalse( result.toString(), result.isAvailable() );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StaticUpdateCheckManager.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StaticUpdateCheckManager.java
new file mode 100644
index 0000000..334d544
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StaticUpdateCheckManager.java
@@ -0,0 +1,87 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.UpdateCheck;
+import org.eclipse.aether.impl.UpdateCheckManager;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.ArtifactNotFoundException;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataNotFoundException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+
+class StaticUpdateCheckManager
+    implements UpdateCheckManager
+{
+
+    private boolean checkRequired;
+
+    private boolean localUpToDate;
+
+    public StaticUpdateCheckManager( boolean checkRequired )
+    {
+        this( checkRequired, !checkRequired );
+    }
+
+    public StaticUpdateCheckManager( boolean checkRequired, boolean localUpToDate )
+    {
+        this.checkRequired = checkRequired;
+        this.localUpToDate = localUpToDate;
+    }
+
+    public void touchMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
+    {
+    }
+
+    public void touchArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
+    {
+    }
+
+    public void checkMetadata( RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check )
+    {
+        check.setRequired( checkRequired );
+
+        if ( check.getLocalLastUpdated() != 0L && localUpToDate )
+        {
+            check.setRequired( false );
+        }
+        if ( !check.isRequired() && !check.getFile().isFile() )
+        {
+            check.setException( new MetadataNotFoundException( check.getItem(), check.getRepository() ) );
+        }
+    }
+
+    public void checkArtifact( RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check )
+    {
+        check.setRequired( checkRequired );
+
+        if ( check.getLocalLastUpdated() != 0L && localUpToDate )
+        {
+            check.setRequired( false );
+        }
+        if ( !check.isRequired() && !check.getFile().isFile() )
+        {
+            check.setException( new ArtifactNotFoundException( check.getItem(), check.getRepository() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRemoteRepositoryManager.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRemoteRepositoryManager.java
new file mode 100644
index 0000000..1836a04
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRemoteRepositoryManager.java
@@ -0,0 +1,67 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.util.StringUtils;
+
+class StubRemoteRepositoryManager
+    implements RemoteRepositoryManager
+{
+
+    public StubRemoteRepositoryManager()
+    {
+    }
+
+    public List<RemoteRepository> aggregateRepositories( RepositorySystemSession session,
+                                                         List<RemoteRepository> dominantRepositories,
+                                                         List<RemoteRepository> recessiveRepositories,
+                                                         boolean recessiveIsRaw )
+    {
+        return dominantRepositories;
+    }
+
+    public RepositoryPolicy getPolicy( RepositorySystemSession session, RemoteRepository repository, boolean releases,
+                                       boolean snapshots )
+    {
+        RepositoryPolicy policy = repository.getPolicy( snapshots );
+
+        String checksums = session.getChecksumPolicy();
+        if ( StringUtils.isEmpty( checksums ) )
+        {
+            checksums = policy.getChecksumPolicy();
+        }
+        String updates = session.getUpdatePolicy();
+        if ( StringUtils.isEmpty( updates ) )
+        {
+            updates = policy.getUpdatePolicy();
+        }
+
+        policy = new RepositoryPolicy( policy.isEnabled(), updates, checksums );
+
+        return policy;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryConnectorProvider.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryConnectorProvider.java
new file mode 100644
index 0000000..3cb5e38
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryConnectorProvider.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.RepositoryConnectorProvider;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+
+class StubRepositoryConnectorProvider
+    implements RepositoryConnectorProvider
+{
+
+    public StubRepositoryConnectorProvider( RepositoryConnector connector )
+    {
+        setConnector( connector );
+    }
+
+    public StubRepositoryConnectorProvider()
+    {
+    }
+
+    private RepositoryConnector connector;
+
+    public void setConnector( RepositoryConnector connector )
+    {
+        this.connector = connector;
+    }
+
+    public RepositoryConnector newRepositoryConnector( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryConnectorException
+    {
+        return connector;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryEventDispatcher.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryEventDispatcher.java
new file mode 100644
index 0000000..b5168e4
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubRepositoryEventDispatcher.java
@@ -0,0 +1,104 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.impl.RepositoryEventDispatcher;
+
+/**
+ */
+public class StubRepositoryEventDispatcher
+    implements RepositoryEventDispatcher
+{
+
+    public void dispatch( RepositoryEvent event )
+    {
+        RepositoryListener listener = event.getSession().getRepositoryListener();
+        if ( listener == null )
+        {
+            return;
+        }
+
+        switch ( event.getType() )
+        {
+            case ARTIFACT_DEPLOYED:
+                listener.artifactDeployed( event );
+                break;
+            case ARTIFACT_DEPLOYING:
+                listener.artifactDeploying( event );
+                break;
+            case ARTIFACT_DESCRIPTOR_INVALID:
+                listener.artifactDescriptorInvalid( event );
+                break;
+            case ARTIFACT_DESCRIPTOR_MISSING:
+                listener.artifactDescriptorMissing( event );
+                break;
+            case ARTIFACT_DOWNLOADED:
+                listener.artifactDownloaded( event );
+                break;
+            case ARTIFACT_DOWNLOADING:
+                listener.artifactDownloading( event );
+                break;
+            case ARTIFACT_INSTALLED:
+                listener.artifactInstalled( event );
+                break;
+            case ARTIFACT_INSTALLING:
+                listener.artifactInstalling( event );
+                break;
+            case ARTIFACT_RESOLVED:
+                listener.artifactResolved( event );
+                break;
+            case ARTIFACT_RESOLVING:
+                listener.artifactResolving( event );
+                break;
+            case METADATA_DEPLOYED:
+                listener.metadataDeployed( event );
+                break;
+            case METADATA_DEPLOYING:
+                listener.metadataDeploying( event );
+                break;
+            case METADATA_DOWNLOADED:
+                listener.metadataDownloaded( event );
+                break;
+            case METADATA_DOWNLOADING:
+                listener.metadataDownloading( event );
+                break;
+            case METADATA_INSTALLED:
+                listener.metadataInstalled( event );
+                break;
+            case METADATA_INSTALLING:
+                listener.metadataInstalling( event );
+                break;
+            case METADATA_INVALID:
+                listener.metadataInvalid( event );
+                break;
+            case METADATA_RESOLVED:
+                listener.metadataResolved( event );
+                break;
+            case METADATA_RESOLVING:
+                listener.metadataResolving( event );
+                break;
+            default:
+                throw new IllegalStateException( "unknown repository event type " + event.getType() );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubSyncContextFactory.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubSyncContextFactory.java
new file mode 100644
index 0000000..91d2988
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubSyncContextFactory.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.SyncContext;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.impl.SyncContextFactory;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * 
+ */
+public class StubSyncContextFactory
+    implements SyncContextFactory
+{
+
+    public SyncContext newInstance( RepositorySystemSession session, boolean shared )
+    {
+        return new SyncContext()
+        {
+            public void close()
+            {
+            }
+
+            public void acquire( Collection<? extends Artifact> artifacts, Collection<? extends Metadata> metadatas )
+            {
+            }
+        };
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionRangeResolver.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionRangeResolver.java
new file mode 100644
index 0000000..ae415d7
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionRangeResolver.java
@@ -0,0 +1,78 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ */
+public class StubVersionRangeResolver
+    implements VersionRangeResolver
+{
+
+    private final VersionScheme versionScheme = new GenericVersionScheme();
+
+    public VersionRangeResult resolveVersionRange( RepositorySystemSession session, VersionRangeRequest request )
+        throws VersionRangeResolutionException
+    {
+        VersionRangeResult result = new VersionRangeResult( request );
+        try
+        {
+            VersionConstraint constraint = versionScheme.parseVersionConstraint( request.getArtifact().getVersion() );
+            result.setVersionConstraint( constraint );
+            if ( constraint.getRange() == null )
+            {
+                result.addVersion( constraint.getVersion() );
+            }
+            else
+            {
+                for ( int i = 1; i < 10; i++ )
+                {
+                    Version ver = versionScheme.parseVersion( Integer.toString( i ) );
+                    if ( constraint.containsVersion( ver ) )
+                    {
+                        result.addVersion( ver );
+                        if ( !request.getRepositories().isEmpty() )
+                        {
+                            result.setRepository( ver, request.getRepositories().get( 0 ) );
+                        }
+                    }
+                }
+            }
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            result.addException( e );
+            throw new VersionRangeResolutionException( result );
+        }
+
+        return result;
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionResolver.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionResolver.java
new file mode 100644
index 0000000..719e5bc
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/StubVersionResolver.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.resolution.VersionRequest;
+import org.eclipse.aether.resolution.VersionResolutionException;
+import org.eclipse.aether.resolution.VersionResult;
+
+/* *
+ */
+class StubVersionResolver
+    implements VersionResolver
+{
+
+    public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
+        throws VersionResolutionException
+    {
+        VersionResult result = new VersionResult( request ).setVersion( request.getArtifact().getVersion() );
+        if ( request.getRepositories().size() > 0 )
+        {
+            result = result.setRepository( request.getRepositories().get( 0 ) );
+        }
+        return result;
+
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/TrackingFileManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/TrackingFileManagerTest.java
new file mode 100644
index 0000000..a3f4bb8
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/TrackingFileManagerTest.java
@@ -0,0 +1,169 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.eclipse.aether.internal.impl.TrackingFileManager;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.junit.Test;
+
+/**
+ */
+public class TrackingFileManagerTest
+{
+
+    @Test
+    public void testRead()
+        throws Exception
+    {
+        TrackingFileManager tfm = new TrackingFileManager();
+
+        File propFile = TestFileUtils.createTempFile( "#COMMENT\nkey1=value1\nkey2 : value2" );
+        Properties props = tfm.read( propFile );
+
+        assertNotNull( props );
+        assertEquals( String.valueOf( props ), 2, props.size() );
+        assertEquals( "value1", props.get( "key1" ) );
+        assertEquals( "value2", props.get( "key2" ) );
+
+        assertTrue( "Leaked file: " + propFile, propFile.delete() );
+
+        props = tfm.read( propFile );
+        assertNull( String.valueOf( props ), props );
+    }
+
+    @Test
+    public void testReadNoFileLeak()
+        throws Exception
+    {
+        TrackingFileManager tfm = new TrackingFileManager();
+
+        for ( int i = 0; i < 1000; i++ )
+        {
+            File propFile = TestFileUtils.createTempFile( "#COMMENT\nkey1=value1\nkey2 : value2" );
+            assertNotNull( tfm.read( propFile ) );
+            assertTrue( "Leaked file: " + propFile, propFile.delete() );
+        }
+    }
+
+    @Test
+    public void testUpdate()
+        throws Exception
+    {
+        TrackingFileManager tfm = new TrackingFileManager();
+
+        // NOTE: The excessive repetitions are to check the update properly truncates the file
+        File propFile = TestFileUtils.createTempFile( "key1=value1\nkey2 : value2\n".getBytes( StandardCharsets.UTF_8 ), 1000 );
+
+        Map<String, String> updates = new HashMap<String, String>();
+        updates.put( "key1", "v" );
+        updates.put( "key2", null );
+
+        tfm.update( propFile, updates );
+
+        Properties props = tfm.read( propFile );
+
+        assertNotNull( props );
+        assertEquals( String.valueOf( props ), 1, props.size() );
+        assertEquals( "v", props.get( "key1" ) );
+        assertNull( String.valueOf( props.get( "key2" ) ), props.get( "key2" ) );
+    }
+
+    @Test
+    public void testUpdateNoFileLeak()
+        throws Exception
+    {
+        TrackingFileManager tfm = new TrackingFileManager();
+
+        Map<String, String> updates = new HashMap<String, String>();
+        updates.put( "k", "v" );
+
+        for ( int i = 0; i < 1000; i++ )
+        {
+            File propFile = TestFileUtils.createTempFile( "#COMMENT\nkey1=value1\nkey2 : value2" );
+            assertNotNull( tfm.update( propFile, updates ) );
+            assertTrue( "Leaked file: " + propFile, propFile.delete() );
+        }
+    }
+
+    @Test
+    public void testLockingOnCanonicalPath()
+        throws Exception
+    {
+        final TrackingFileManager tfm = new TrackingFileManager();
+
+        final File propFile = TestFileUtils.createTempFile( "#COMMENT\nkey1=value1\nkey2 : value2" );
+
+        final List<Throwable> errors = Collections.synchronizedList( new ArrayList<Throwable>() );
+
+        Thread[] threads = new Thread[4];
+        for ( int i = 0; i < threads.length; i++ )
+        {
+            String path = propFile.getParent();
+            for ( int j = 0; j < i; j++ )
+            {
+                path += "/.";
+            }
+            path += "/" + propFile.getName();
+            final File file = new File( path );
+
+            threads[i] = new Thread()
+            {
+                public void run()
+                {
+                    try
+                    {
+                        for ( int i = 0; i < 1000; i++ )
+                        {
+                            assertNotNull( tfm.read( file ) );
+                        }
+                    }
+                    catch ( Throwable e )
+                    {
+                        errors.add( e );
+                    }
+                }
+            };
+        }
+
+        for ( Thread thread1 : threads )
+        {
+            thread1.start();
+        }
+
+        for ( Thread thread : threads )
+        {
+            thread.join();
+        }
+
+        assertEquals( Collections.emptyList(), errors );
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java
new file mode 100644
index 0000000..7f64e06
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java
@@ -0,0 +1,94 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
+import org.eclipse.aether.transfer.ChecksumFailureException;
+import org.eclipse.aether.transfer.TransferResource;
+import org.junit.Before;
+import org.junit.Test;
+
+public class WarnChecksumPolicyTest
+{
+
+    private WarnChecksumPolicy policy;
+
+    private ChecksumFailureException exception;
+
+    @Before
+    public void setup()
+    {
+        policy = new WarnChecksumPolicy( null, new TransferResource( "null", "file:/dev/null", "file.txt", null, null ) );
+        exception = new ChecksumFailureException( "test" );
+    }
+
+    @Test
+    public void testOnTransferChecksumFailure()
+    {
+        assertTrue( policy.onTransferChecksumFailure( exception ) );
+    }
+
+    @Test
+    public void testOnChecksumMatch()
+    {
+        assertTrue( policy.onChecksumMatch( "SHA-1", 0 ) );
+        assertTrue( policy.onChecksumMatch( "SHA-1", ChecksumPolicy.KIND_UNOFFICIAL ) );
+    }
+
+    @Test
+    public void testOnChecksumMismatch()
+        throws Exception
+    {
+        try
+        {
+            policy.onChecksumMismatch( "SHA-1", 0, exception );
+            fail( "No exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertSame( exception, e );
+        }
+        policy.onChecksumMismatch( "SHA-1", ChecksumPolicy.KIND_UNOFFICIAL, exception );
+    }
+
+    @Test
+    public void testOnChecksumError()
+        throws Exception
+    {
+        policy.onChecksumError( "SHA-1", 0, exception );
+    }
+
+    @Test
+    public void testOnNoMoreChecksums()
+    {
+        try
+        {
+            policy.onNoMoreChecksums();
+            fail( "No exception" );
+        }
+        catch ( ChecksumFailureException e )
+        {
+            assertTrue( e.getMessage().contains( "no checksums available" ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..512f92d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.0-SNAPSHOT.ini
@@ -0,0 +1,56 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:86:pom:7.13.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:349:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..406175c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_117_4.1-SNAPSHOT.ini
@@ -0,0 +1,58 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:86:pom:7.13.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:349:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..bb59696
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+10:12:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:208:pom:10.0-SNAPSHOT
+1:264:pom:6.2-SNAPSHOT
+1:371:pom:822-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..cce6222
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_11_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+10:12:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:208:pom:10.0-SNAPSHOT
+1:264:pom:6.2-SNAPSHOT
+1:371:pom:822-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..1a99901
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.0-SNAPSHOT.ini
@@ -0,0 +1,65 @@
+[dependencies]
+1:122:pom:3.1.1-SNAPSHOT
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:124:pom:2.2-SNAPSHOT
+1:125:pom:8.1-SNAPSHOT
+1:125:pom:9.1-SNAPSHOT
+1:125:pom:9.5-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:126:pom:2.81-SNAPSHOT
+1:126:pom:2.90-SNAPSHOT
+1:126:pom:3.50-SNAPSHOT
+1:127:pom:6.20-SNAPSHOT
+1:127:pom:6.30-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:128:pom:2006-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:129:pom:10.1-SNAPSHOT
+1:130:pom:10.1-SNAPSHOT
+1:130:pom:10.2-SNAPSHOT
+1:130:pom:11.1-SNAPSHOT
+1:130:pom:9.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:131:pom:5.0-SNAPSHOT
+1:131:pom:5.3-SNAPSHOT
+1:131:pom:6.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:132:pom:7.7-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:133:pom:12.5-SNAPSHOT
+1:133:pom:15.0-SNAPSHOT
+1:134:pom:12.6-SNAPSHOT
+1:134:pom:12.7-SNAPSHOT
+1:135:pom:10.0-SNAPSHOT
+1:135:pom:11.0-SNAPSHOT
+1:136:pom:3.04-SNAPSHOT
+1:136:pom:3.06-SNAPSHOT
+1:137:pom:2.2.12-SNAPSHOT
+1:138:pom:7.7.06-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:163:pom:4.0-SNAPSHOT
+10:164:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+1:32:pom:720-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..41ae285
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_121_3.1-SNAPSHOT.ini
@@ -0,0 +1,76 @@
+[dependencies]
+1:122:pom:3.1.1-SNAPSHOT
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:124:pom:2.2-SNAPSHOT
+1:125:pom:8.1-SNAPSHOT
+1:125:pom:9.1-SNAPSHOT
+1:125:pom:9.5-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:126:pom:2.81-SNAPSHOT
+1:126:pom:2.90-SNAPSHOT
+1:126:pom:3.50-SNAPSHOT
+1:127:pom:6.20-SNAPSHOT
+1:127:pom:6.30-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:128:pom:2006-SNAPSHOT
+1:196:pom:1.2.12-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:129:pom:10.1-SNAPSHOT
+1:130:pom:10.1-SNAPSHOT
+1:130:pom:10.2-SNAPSHOT
+1:130:pom:11.1-SNAPSHOT
+1:130:pom:9.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:131:pom:5.0-SNAPSHOT
+1:131:pom:5.3-SNAPSHOT
+1:131:pom:6.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:132:pom:7.7-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:133:pom:12.5-SNAPSHOT
+1:133:pom:15.0-SNAPSHOT
+1:133:pom:15.5-SNAPSHOT
+1:134:pom:12.6-SNAPSHOT
+1:134:pom:12.7-SNAPSHOT
+1:135:pom:10.0-SNAPSHOT
+1:135:pom:11.0-SNAPSHOT
+1:136:pom:3.04-SNAPSHOT
+1:136:pom:3.06-SNAPSHOT
+1:137:pom:2.2.12-SNAPSHOT
+1:138:pom:7.7.06-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:164:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:159:pom:2.1_03-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:311:pom:2.3.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..eb29c95
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.0-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:35:pom:1.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:7:pom:5.8.9-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:263:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..48a697b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_12_4.1-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:34:pom:1.13-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:35:pom:1.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:7:pom:5.8.9-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:263:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..7d05704
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.0-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0f788bd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_139_3.1-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:4.2.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..9cac870
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.0-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:142:pom:9.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:138:pom:7.7.06-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:144:pom:15.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:150:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..c2bbe43
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_141_4.1-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:142:pom:9.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:138:pom:7.7.06-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:144:pom:15.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:150:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..8a533b1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.0-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0c746dc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_145_4.1-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..b8eb5de
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.0-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0214124
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_146_4.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e99272d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.0-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:144:pom:15.0-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..66c8cf1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_147_4.1-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:144:pom:15.0-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..ad21c3f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.0-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..02ac765
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_148_4.1-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+10:11:pom:4.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..56d68df
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.0-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..821ee3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_149_4.1-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..5c3f4f3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.0-SNAPSHOT.ini
@@ -0,0 +1,46 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:49:pom:6.0.5.25-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:35:pom:1.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:151:pom:3.7.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:162:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..dd9141e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_150_4.1-SNAPSHOT.ini
@@ -0,0 +1,45 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:49:pom:6.0.5.25-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:35:pom:1.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:151:pom:3.7.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:162:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2fa7c3b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.0-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:155:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..945ce3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_152_4.1-SNAPSHOT.ini
@@ -0,0 +1,34 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:155:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+10:363:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3861a88
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..eb76665
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_155_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..66fdca2
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.0-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:30:pom:0.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:157:pom:0.0.356_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:159:pom:2.1_03-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..a728a51
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_156_4.1-SNAPSHOT.ini
@@ -0,0 +1,34 @@
+[dependencies]
+1:30:pom:0.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:157:pom:0.0.356_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:159:pom:2.1_03-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..00a6c60
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.0-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..00a6c60
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_160_4.1-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..09ddcca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.0-SNAPSHOT.ini
@@ -0,0 +1,14 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:162:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..8b428a0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_161_4.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:162:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3c2ea75
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..711a9ef
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_162_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..19ce732
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:8:pom:2.1.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:101:pom:1.0_sap.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..dbd663e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_163_4.1-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:8:pom:2.1.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:101:pom:1.0_sap.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..9974406
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.0-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:165:pom:11.1-SNAPSHOT
+1:165:pom:7.0-SNAPSHOT
+1:165:pom:7.1-SNAPSHOT
+1:165:pom:9.3-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:166:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..2bdaa0d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_164_4.1-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:165:pom:11.1-SNAPSHOT
+1:165:pom:7.0-SNAPSHOT
+1:165:pom:7.1-SNAPSHOT
+1:165:pom:9.3-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:166:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2f03ade
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.0-SNAPSHOT.ini
@@ -0,0 +1,69 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:168:pom:6.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:169:pom:4.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:142:pom:9.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:6.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:170:pom:10.0-SNAPSHOT
+1:95:pom:7.0-SNAPSHOT
+1:97:pom:3.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:11.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:171:pom:8.45-SNAPSHOT
+1:171:pom:8.46-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:100:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:163:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:225:pom:4.0-SNAPSHOT
+10:224:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:266:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..8a14bee
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_167_4.1-SNAPSHOT.ini
@@ -0,0 +1,69 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:168:pom:6.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:169:pom:4.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:142:pom:9.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:6.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:170:pom:10.0-SNAPSHOT
+1:95:pom:7.0-SNAPSHOT
+1:97:pom:3.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:11.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:171:pom:8.45-SNAPSHOT
+1:171:pom:8.46-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:100:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:163:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:225:pom:4.1-SNAPSHOT
+10:224:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:266:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..77aefdc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.0-SNAPSHOT.ini
@@ -0,0 +1,45 @@
+[dependencies]
+10:173:pom:4.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:164:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:39:pom:0.9.8l-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..4d7161e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_172_4.1-SNAPSHOT.ini
@@ -0,0 +1,46 @@
+[dependencies]
+10:173:pom:4.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:164:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:39:pom:0.9.8l-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f3fbfc4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f3fbfc4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_173_4.1-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f8b9876
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.0-SNAPSHOT.ini
@@ -0,0 +1,81 @@
+[dependencies]
+1:30:pom:0.7.0-SNAPSHOT
+10:175:pom:2.0-SNAPSHOT
+10:173:pom:4.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:189:pom:4.0-SNAPSHOT
+10:202:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:203:pom:4.0-SNAPSHOT
+10:176:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:345:pom:4.0-SNAPSHOT
+10:178:pom:4.0-SNAPSHOT
+10:179:pom:4.0-SNAPSHOT
+10:346:pom:4.0-SNAPSHOT
+10:347:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:348:pom:3.8.0-SNAPSHOT
+1:195:pom:3.2-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:342:pom:1.2-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:320:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..36fcb41
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_174_4.1-SNAPSHOT.ini
@@ -0,0 +1,86 @@
+[dependencies]
+1:30:pom:0.7.0-SNAPSHOT
+10:175:pom:2.1-SNAPSHOT
+10:173:pom:4.1-SNAPSHOT
+10:360:pom:1.0-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:202:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:203:pom:4.1-SNAPSHOT
+10:176:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:345:pom:4.1-SNAPSHOT
+10:178:pom:4.1-SNAPSHOT
+10:179:pom:4.1-SNAPSHOT
+10:346:pom:4.1-SNAPSHOT
+10:347:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:348:pom:3.8.0-SNAPSHOT
+1:370:pom:6.0-SNAPSHOT
+1:195:pom:3.2-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:342:pom:1.2-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:320:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..934a1f6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.0-SNAPSHOT.ini
@@ -0,0 +1,23 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
+10:176:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:178:pom:4.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:75:pom:25-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:47:pom:2.9.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3fa5750
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_175_2.1-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+10:11:pom:4.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:47:pom:2.9.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..0ad7148
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9fb6281
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_176_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..be70db4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..2791c06
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_177_4.1-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..1d22cc9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.0-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:179:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..fe5c632
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_178_4.1-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:179:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f848575
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.0-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..21487ce
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_179_4.1-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3054e90
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.1-SNAPSHOT.ini
@@ -0,0 +1,30 @@
+[dependencies]
+1:181:pom:1.0-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:183:pom:0.9-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:186:pom:4.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:187:pom:4.3-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..0a52122
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_180_3.2-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:181:pom:1.0-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:183:pom:0.9-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:186:pom:4.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:187:pom:4.3-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..8b9b40c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.0-SNAPSHOT.ini
@@ -0,0 +1,44 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:190:pom:2.0.8-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:191:pom:3.2-SNAPSHOT
+1:192:pom:1.8.0.7-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:193:pom:0.8.1-SNAPSHOT
+1:194:pom:2.3.0-SNAPSHOT
+1:195:pom:3.2-SNAPSHOT
+1:196:pom:1.2.12-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:198:pom:9.1.3-SNAPSHOT
+1:199:pom:2.2-SNAPSHOT
+1:187:pom:4.3-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:200:pom:3.1.12-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1cac10a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_189_4.1-SNAPSHOT.ini
@@ -0,0 +1,46 @@
+[dependencies]
+1:123:pom:3.1.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:190:pom:2.0.8-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:191:pom:3.2-SNAPSHOT
+1:192:pom:1.8.0.7-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:193:pom:0.8.1-SNAPSHOT
+1:194:pom:2.3.0-SNAPSHOT
+1:195:pom:3.2-SNAPSHOT
+1:196:pom:1.2.12-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:198:pom:9.1.3-SNAPSHOT
+1:199:pom:2.2-SNAPSHOT
+1:187:pom:4.3-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:200:pom:3.1.12-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..1329b77
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.0-SNAPSHOT.ini
@@ -0,0 +1,28 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:189:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e5c28b1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_202_4.1-SNAPSHOT.ini
@@ -0,0 +1,29 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3a7a786
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.0-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:275:pom:4.0-SNAPSHOT
+10:276:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:284:pom:2.0-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..45824de
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_203_4.1-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:275:pom:4.1-SNAPSHOT
+10:276:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:284:pom:2.1-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..636848a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.0-SNAPSHOT.ini
@@ -0,0 +1,99 @@
+[dependencies]
+10:121:pom:3.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:206:pom:4.0-SNAPSHOT
+10:216:pom:4.0-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:287:pom:1.1.2.1-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:87:pom:6b-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:338:pom:2.8-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:339:pom:8.0.1p5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:283:pom:1.0-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:340:pom:2.9-SNAPSHOT
+1:341:pom:0.9-SNAPSHOT
+1:114:pom:2.50.28_sap.1-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:342:pom:1.2-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:343:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+10:320:pom:4.0-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
+10:305:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+1:151:pom:3.7.1-SNAPSHOT
+1:344:pom:1.6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f18be4d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_205_4.1-SNAPSHOT.ini
@@ -0,0 +1,104 @@
+[dependencies]
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:206:pom:4.1-SNAPSHOT
+10:216:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:287:pom:1.1.2.1-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:87:pom:6b-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:338:pom:3.0-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:143:pom:6.0-SNAPSHOT
+1:368:pom:4.7.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:339:pom:8.0.1p5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:369:pom:2.4.5-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:283:pom:1.0-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:340:pom:2.11-SNAPSHOT
+1:341:pom:1.3-SNAPSHOT
+1:114:pom:2.50.28_sap.1-SNAPSHOT
+1:120:pom:1.0-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:342:pom:1.2-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:343:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+10:320:pom:4.1-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
+10:305:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+1:151:pom:3.7.1-SNAPSHOT
+1:344:pom:1.6.0-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..8860f27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.0-SNAPSHOT.ini
@@ -0,0 +1,12 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:207:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:214:pom:4.0-SNAPSHOT
+10:215:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0056481
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_206_4.1-SNAPSHOT.ini
@@ -0,0 +1,12 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:207:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:214:pom:4.1-SNAPSHOT
+10:215:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..34d148b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.0-SNAPSHOT.ini
@@ -0,0 +1,29 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:208:pom:10.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:95:pom:7.1-SNAPSHOT
+1:95:pom:8.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:209:pom:3.51-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:210:pom:4.0-SNAPSHOT
+10:213:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..913795c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_207_4.1-SNAPSHOT.ini
@@ -0,0 +1,29 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:208:pom:10.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:95:pom:7.1-SNAPSHOT
+1:95:pom:8.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:209:pom:3.51-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:210:pom:4.1-SNAPSHOT
+10:213:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c846c48
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..4498c9a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_210_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..927422d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..fd2d466
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_212_4.1-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..5e1bbff
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:210:pom:4.0-SNAPSHOT
+10:207:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..716cc85
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_213_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:210:pom:4.1-SNAPSHOT
+10:207:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..337146b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:210:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e4c878d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_214_4.1-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:210:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..cc693b8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:214:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b576be2
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_215_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:214:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..0b37b78
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.0-SNAPSHOT.ini
@@ -0,0 +1,48 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:285:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:298:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:263:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:337:pom:4.0-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..ea48715
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_216_4.1-SNAPSHOT.ini
@@ -0,0 +1,48 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:285:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:298:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:263:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:337:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3bfab2b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.0-SNAPSHOT.ini
@@ -0,0 +1,53 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:221:pom:8.9-SNAPSHOT
+1:222:pom:1.1.1-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:223:pom:7.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:224:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:229:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:262:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0e34cad
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_220_4.1-SNAPSHOT.ini
@@ -0,0 +1,54 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:221:pom:8.9-SNAPSHOT
+1:222:pom:1.1.1-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:223:pom:7.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:224:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:229:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:262:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..74fdf31
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.0-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:221:pom:8.9-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:223:pom:7.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:225:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..4323e85
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_224_4.1-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:221:pom:8.9-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:223:pom:7.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:225:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..241db59
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.0-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:163:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..bb96add
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_225_4.1-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:163:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e1ef8cd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.0-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:227:pom:7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..71fe698
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_226_4.1-SNAPSHOT.ini
@@ -0,0 +1,36 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:227:pom:7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_228_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_228_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..59958f6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_228_1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+10:139:pom:3.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..7172d1c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.0-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:230:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0ae9cd0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_229_4.1-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:230:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2e07aa7
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.0-SNAPSHOT.ini
@@ -0,0 +1,23 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61d2cfd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_22_4.1-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..65e3aa0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.0-SNAPSHOT.ini
@@ -0,0 +1,41 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:227:pom:7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:216:pom:4.0-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:337:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9b5668f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_230_4.1-SNAPSHOT.ini
@@ -0,0 +1,41 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:227:pom:7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:216:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:337:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..7b06419
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..c0a8f5d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_231_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..bb701f4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.0-SNAPSHOT.ini
@@ -0,0 +1,28 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:263:pom:4.0-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3c4f0d3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_232_4.1-SNAPSHOT.ini
@@ -0,0 +1,28 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:263:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a0e0ab1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.0-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:79:pom:0.7.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:234:pom:1.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..a260894
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_233_4.1-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:79:pom:0.7.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:234:pom:1.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c494dca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.0-SNAPSHOT.ini
@@ -0,0 +1,36 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:155:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:150:pom:4.0-SNAPSHOT
+10:236:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b1b930e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_235_4.1-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:155:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:150:pom:4.1-SNAPSHOT
+10:236:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f927516
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.0-SNAPSHOT.ini
@@ -0,0 +1,20 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:237:pom:4.0-SNAPSHOT
+10:150:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:162:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..36996ed
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_236_4.1-SNAPSHOT.ini
@@ -0,0 +1,20 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:237:pom:4.1-SNAPSHOT
+10:150:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:162:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..8654840
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d356e0b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_237_4.1-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..62b1d93
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.0-SNAPSHOT.ini
@@ -0,0 +1,39 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:241:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:155:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:150:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:298:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:317:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..02f56fc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_238_4.1-SNAPSHOT.ini
@@ -0,0 +1,39 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:241:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:155:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:150:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:298:pom:4.1-SNAPSHOT
+10:363:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:317:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..05973bb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..2c1e64d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_241_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..b05a852
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.0-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:150:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:262:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9fad2e3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_242_4.1-SNAPSHOT.ini
@@ -0,0 +1,30 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:150:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:262:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d979d16
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.0-SNAPSHOT.ini
@@ -0,0 +1,30 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:244:pom:1.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:245:pom:1.0.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9b97a57
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_243_4.1-SNAPSHOT.ini
@@ -0,0 +1,34 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:358:pom:1.1.2-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:244:pom:1.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:245:pom:1.0.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:363:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:364:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2bcf940
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.0-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f71da7a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_246_4.1-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..7100a80
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.0-SNAPSHOT.ini
@@ -0,0 +1,54 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:244:pom:1.2-SNAPSHOT
+1:248:pom:5.5-SNAPSHOT
+1:249:pom:9.2.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:200:pom:5.1-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b37cc1f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_247_4.1-SNAPSHOT.ini
@@ -0,0 +1,55 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:244:pom:1.2-SNAPSHOT
+1:248:pom:5.5-SNAPSHOT
+1:249:pom:9.2.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:200:pom:5.1-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..611fe39
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.0-SNAPSHOT.ini
@@ -0,0 +1,23 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:252:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b90b8dc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_251_4.1-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:358:pom:1.1.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:252:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..6911906
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.0-SNAPSHOT.ini
@@ -0,0 +1,28 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:253:pom:1.3.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:254:pom:4.0-SNAPSHOT
+10:255:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d4b3bef
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_252_4.1-SNAPSHOT.ini
@@ -0,0 +1,29 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:253:pom:1.3.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:361:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:254:pom:4.1-SNAPSHOT
+10:255:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_254_4.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..92db689
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.0-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:181:pom:1.0-SNAPSHOT
+1:256:pom:3.2-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..92db689
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_255_4.1-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:181:pom:1.0-SNAPSHOT
+1:256:pom:3.2-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..dea2f0e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.0-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:258:pom:4.0-SNAPSHOT
+1:259:pom:2.0-SNAPSHOT
+1:260:pom:2.2.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..cae030f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_257_4.1-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:258:pom:4.1-SNAPSHOT
+1:259:pom:2.0-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..df77fce
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.0-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+10:212:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d3c0466
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_258_4.1-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+10:212:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..80b0949
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.0-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b4e12a9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_261_4.1-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..825bfdc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.0-SNAPSHOT.ini
@@ -0,0 +1,44 @@
+[dependencies]
+10:42:pom:4.0-SNAPSHOT
+10:263:pom:4.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:7:pom:5.8.9-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:264:pom:6.2-SNAPSHOT
+1:166:pom:720-SNAPSHOT
+10:265:pom:4.0-SNAPSHOT
+1:267:pom:3.0-SNAPSHOT
+10:268:pom:4.0-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:270:pom:1.6.5-SNAPSHOT
+10:225:pom:4.0-SNAPSHOT
+10:271:pom:4.0-SNAPSHOT
+10:297:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..79099d0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_262_4.1-SNAPSHOT.ini
@@ -0,0 +1,45 @@
+[dependencies]
+10:42:pom:4.1-SNAPSHOT
+10:263:pom:4.1-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:7:pom:5.8.9-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:264:pom:6.2-SNAPSHOT
+1:166:pom:720-SNAPSHOT
+10:265:pom:4.1-SNAPSHOT
+1:267:pom:3.0-SNAPSHOT
+10:268:pom:4.1-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:270:pom:1.6.5-SNAPSHOT
+10:225:pom:4.1-SNAPSHOT
+10:271:pom:4.1-SNAPSHOT
+10:297:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..6097205
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..64997c1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_263_4.1-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..157eaf5
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.0-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:165:pom:7.0-SNAPSHOT
+1:165:pom:7.1-SNAPSHOT
+1:165:pom:9.3-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:114:pom:2.50.16.busObj.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:266:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..081169f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_265_4.1-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:165:pom:7.0-SNAPSHOT
+1:165:pom:7.1-SNAPSHOT
+1:165:pom:9.3-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:114:pom:2.50.16.busObj.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:266:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_266_4.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..6ec73f2
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:269:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3dc7ebc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_268_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:269:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..499dbac
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.0-SNAPSHOT.ini
@@ -0,0 +1,16 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..342874a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_269_4.1-SNAPSHOT.ini
@@ -0,0 +1,16 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..005e05e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.0-SNAPSHOT.ini
@@ -0,0 +1,62 @@
+[dependencies]
+10:174:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:203:pom:4.0-SNAPSHOT
+10:272:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:285:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:286:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:290:pom:8.0.2.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:189:pom:4.0-SNAPSHOT
+10:202:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+10:291:pom:4.0-SNAPSHOT
+10:292:pom:4.0-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:296:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b84bd73
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_271_4.1-SNAPSHOT.ini
@@ -0,0 +1,65 @@
+[dependencies]
+10:174:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:203:pom:4.1-SNAPSHOT
+10:272:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:285:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:286:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:290:pom:8.0.2.0-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:202:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+10:291:pom:4.1-SNAPSHOT
+10:292:pom:4.1-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:296:pom:1.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f0a6411
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.0-SNAPSHOT.ini
@@ -0,0 +1,41 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:273:pom:1.0.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:274:pom:1.0.0-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:275:pom:4.0-SNAPSHOT
+10:203:pom:4.0-SNAPSHOT
+10:276:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:216:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e6265b8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_272_4.1-SNAPSHOT.ini
@@ -0,0 +1,41 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:273:pom:1.0.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:274:pom:1.0.0-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:275:pom:4.1-SNAPSHOT
+10:203:pom:4.1-SNAPSHOT
+10:276:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:216:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..9af9506
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:276:pom:4.0-SNAPSHOT
+10:216:pom:4.0-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:284:pom:2.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..78b02f7
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_275_4.1-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:276:pom:4.1-SNAPSHOT
+10:216:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:284:pom:2.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f3215b8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.0-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:175:pom:2.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:175:pom:2.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:277:pom:4.0-SNAPSHOT
+10:280:pom:4.0-SNAPSHOT
+10:281:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e77a448
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_276_4.1-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:175:pom:2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_277_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_277_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..32817bd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_277_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:278:pom:1.8.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:279:pom:4.0-SNAPSHOT
+10:280:pom:4.0-SNAPSHOT
+10:281:pom:4.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_279_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_279_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..bcece4b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_279_4.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_280_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_280_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..632411e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_280_4.0-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+10:279:pom:4.0-SNAPSHOT
+10:281:pom:4.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:278:pom:1.8.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:282:pom:2.0-SNAPSHOT
+1:283:pom:1.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:279:pom:4.0-SNAPSHOT
+10:277:pom:4.0-SNAPSHOT
+10:281:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_281_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_281_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2d02b8b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_281_4.0-SNAPSHOT.ini
@@ -0,0 +1,12 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+10:279:pom:4.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:279:pom:4.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..651b3eb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:204:pom:1.6.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.18-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e49da21
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_284_2.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+10:11:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e6ac42f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.0-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0bab48a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_285_4.1-SNAPSHOT.ini
@@ -0,0 +1,20 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..5fed984
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.0-SNAPSHOT.ini
@@ -0,0 +1,45 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:287:pom:1.1.2.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:288:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..46ea57f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_286_4.1-SNAPSHOT.ini
@@ -0,0 +1,45 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:287:pom:1.1.2.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:288:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d788c25
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.0-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..db3c523
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_288_4.1-SNAPSHOT.ini
@@ -0,0 +1,25 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a3dfe58
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.0-SNAPSHOT.ini
@@ -0,0 +1,69 @@
+[dependencies]
+1:181:pom:1.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..db6e739
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_291_4.1-SNAPSHOT.ini
@@ -0,0 +1,74 @@
+[dependencies]
+10:242:pom:4.1-SNAPSHOT
+1:181:pom:1.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:365:pom:3.4.1-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+1:365:pom:3.4.1-SNAPSHOT
+10:366:pom:4.1-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..69fc9f9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.0-SNAPSHOT.ini
@@ -0,0 +1,47 @@
+[dependencies]
+1:182:pom:3.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:293:pom:0.2.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:294:pom:4.0-SNAPSHOT
+10:295:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d77271d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_292_4.1-SNAPSHOT.ini
@@ -0,0 +1,48 @@
+[dependencies]
+1:182:pom:3.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:197:pom:5.1.3-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.4.C-SNAPSHOT
+1:293:pom:0.2.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:294:pom:4.1-SNAPSHOT
+10:295:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:32:pom:720-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d32b540
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.0-SNAPSHOT.ini
@@ -0,0 +1,26 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:295:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3a2d866
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_294_4.1-SNAPSHOT.ini
@@ -0,0 +1,27 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:295:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e9c0f6b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.0-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..256b19f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_295_4.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..4a8f0bc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.0-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+10:220:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:298:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+1:299:pom:7.20-SNAPSHOT
+1:300:pom:7.1.8-SNAPSHOT
+10:301:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:306:pom:4.0-SNAPSHOT
+10:307:pom:4.0-SNAPSHOT
+10:203:pom:4.0-SNAPSHOT
+10:316:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..8e71771
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_297_4.1-SNAPSHOT.ini
@@ -0,0 +1,32 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:298:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+1:299:pom:7.20-SNAPSHOT
+10:301:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:306:pom:4.1-SNAPSHOT
+10:307:pom:4.1-SNAPSHOT
+10:203:pom:4.1-SNAPSHOT
+10:316:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..1c97dd8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.0-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:288:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..efab19c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_298_4.1-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:288:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..9626f66
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.0-SNAPSHOT.ini
@@ -0,0 +1,47 @@
+[dependencies]
+1:302:pom:1.015-SNAPSHOT
+1:73:pom:2.3.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:181:pom:1.0-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+10:303:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:189:pom:4.0-SNAPSHOT
+10:202:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:305:pom:4.0-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b5d06af
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_301_4.1-SNAPSHOT.ini
@@ -0,0 +1,49 @@
+[dependencies]
+1:302:pom:1.015-SNAPSHOT
+1:73:pom:2.3.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:153:pom:1.1.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:61:pom:1.2.12-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:181:pom:1.0-SNAPSHOT
+1:182:pom:3.2-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+10:303:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:202:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:305:pom:4.1-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e3c6a74
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.0-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:304:pom:9.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e3c6a74
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_303_4.1-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:304:pom:9.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3610429
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.0-SNAPSHOT.ini
@@ -0,0 +1,42 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.4-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:287:pom:1.3-SNAPSHOT
+1:222:pom:1.1.1-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d9287b0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_305_4.1-SNAPSHOT.ini
@@ -0,0 +1,43 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.4-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:287:pom:1.3-SNAPSHOT
+1:222:pom:1.1.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..6d1f2a3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.0-SNAPSHOT.ini
@@ -0,0 +1,36 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:230:pom:4.0-SNAPSHOT
+10:226:pom:4.0-SNAPSHOT
+10:231:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:285:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..41467e9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_306_4.1-SNAPSHOT.ini
@@ -0,0 +1,36 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:230:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:285:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c495bb1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.0-SNAPSHOT.ini
@@ -0,0 +1,63 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:308:pom:1.8-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:309:pom:1.15.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:310:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:311:pom:2.3.0-SNAPSHOT
+1:312:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:313:pom:2.2-SNAPSHOT
+1:314:pom:6.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+10:305:pom:4.0-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..59b3b89
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_307_4.1-SNAPSHOT.ini
@@ -0,0 +1,64 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:308:pom:1.8-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:309:pom:1.15.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:310:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:188:pom:1.9-SNAPSHOT
+1:311:pom:2.3.0-SNAPSHOT
+1:312:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:313:pom:2.2-SNAPSHOT
+1:314:pom:6.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+10:305:pom:4.1-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e6e9bef
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.0-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:202:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:203:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..50bb544
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_315_4.1-SNAPSHOT.ini
@@ -0,0 +1,38 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:211:pom:1.0-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:202:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:203:pom:4.1-SNAPSHOT
+1:367:pom:8.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..b8afaed
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:232:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9639623
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_316_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..db57585
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.0-SNAPSHOT.ini
@@ -0,0 +1,20 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:321:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:118:pom:10.5.3.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1cd43be
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_317_4.1-SNAPSHOT.ini
@@ -0,0 +1,20 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:321:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:118:pom:10.5.3.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..84fda91
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.0-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+10:173:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..11992b8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_318_4.1-SNAPSHOT.ini
@@ -0,0 +1,22 @@
+[dependencies]
+10:173:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61d1f3f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.0-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+10:173:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:320:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..69e9d08
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_319_4.1-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+10:173:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:320:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..e5274a8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.0-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:173:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3b1f588
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_320_4.1-SNAPSHOT.ini
@@ -0,0 +1,18 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:173:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
+1:201:pom:3.5.2-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..521563c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.0-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:15:pom:1.36.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:322:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..224a33e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_321_4.1-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:15:pom:1.36.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:322:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..13f3b0a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.0-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:82:pom:9.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:85:pom:6.4-SNAPSHOT
+1:85:pom:9.1-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:26:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:324:pom:5.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:225:pom:4.0-SNAPSHOT
+10:325:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:335:pom:4.0-SNAPSHOT
+10:336:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..bf492b7
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_322_4.1-SNAPSHOT.ini
@@ -0,0 +1,31 @@
+[dependencies]
+1:82:pom:9.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:85:pom:6.4-SNAPSHOT
+1:85:pom:9.1-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:26:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:324:pom:5.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:225:pom:4.1-SNAPSHOT
+10:325:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:335:pom:4.1-SNAPSHOT
+10:336:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..861c27c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.0-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:326:pom:0.9.7-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:207:pom:4.0-SNAPSHOT
+10:327:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:329:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:331:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:332:pom:4.0-SNAPSHOT
+10:330:pom:4.0-SNAPSHOT
+10:333:pom:4.0-SNAPSHOT
+10:334:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..29005f4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_325_4.1-SNAPSHOT.ini
@@ -0,0 +1,33 @@
+[dependencies]
+1:326:pom:0.9.7-SNAPSHOT
+1:24:pom:1.2.10-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:240:pom:720-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:207:pom:4.1-SNAPSHOT
+10:327:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:329:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:331:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:332:pom:4.1-SNAPSHOT
+10:330:pom:4.1-SNAPSHOT
+10:333:pom:4.1-SNAPSHOT
+10:334:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..da3b6e2
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.0-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:328:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e2bfac8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_327_4.1-SNAPSHOT.ini
@@ -0,0 +1,21 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:328:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..60b3e5f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.0-SNAPSHOT.ini
@@ -0,0 +1,53 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:291:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:288:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:286:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..33b7339
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_328_4.1-SNAPSHOT.ini
@@ -0,0 +1,53 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:291:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:288:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:286:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d1fa191
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.0-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:212:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:330:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..c931d50
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_329_4.1-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:24:pom:1.2.10-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:212:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:330:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c2c4561
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.0-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..c2c4561
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_330_4.1-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..7c00f7e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.0-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:288:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f68cbb3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_331_4.1-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:288:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..9f5e6d6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.0-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+10:238:pom:4.0-SNAPSHOT
+10:331:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+10:330:pom:4.0-SNAPSHOT
+10:333:pom:4.0-SNAPSHOT
+10:334:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1e32f44
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_332_4.1-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+10:238:pom:4.1-SNAPSHOT
+10:331:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+10:330:pom:4.1-SNAPSHOT
+10:333:pom:4.1-SNAPSHOT
+10:334:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_333_4.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..2be90d3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.0-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:238:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:331:pom:4.0-SNAPSHOT
+10:288:pom:4.0-SNAPSHOT
+10:286:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b5897b1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_334_4.1-SNAPSHOT.ini
@@ -0,0 +1,24 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:118:pom:10.2.2.0-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:239:pom:3.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:154:pom:3.3-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:331:pom:4.1-SNAPSHOT
+10:288:pom:4.1-SNAPSHOT
+10:286:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c291a39
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.0-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f268bc5
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_335_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+1:16:pom:1.8.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..29f58e8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.0-SNAPSHOT.ini
@@ -0,0 +1,16 @@
+[dependencies]
+1:326:pom:0.9.7-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:26:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:102:pom:1.busObj.1-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:322:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..13f3f91
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_336_4.1-SNAPSHOT.ini
@@ -0,0 +1,16 @@
+[dependencies]
+1:326:pom:0.9.7-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:26:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:94:pom:4.0-SNAPSHOT
+1:323:pom:11.0-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:102:pom:1.busObj.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:322:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..05b1294
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.0-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..05b1294
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_337_4.1-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..df77fce
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.0-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+10:212:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d3c0466
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_343_4.1-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+10:212:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..3f3b4d4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.0-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:278:pom:1.8.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:176:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:179:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:175:pom:2.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..6d34fde
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_345_4.1-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:3.8.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:278:pom:1.8.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:176:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:179:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:175:pom:2.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..baa1676
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.0-SNAPSHOT.ini
@@ -0,0 +1,14 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:178:pom:4.0-SNAPSHOT
+10:179:pom:4.0-SNAPSHOT
+10:347:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e53daf9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_346_4.1-SNAPSHOT.ini
@@ -0,0 +1,14 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:178:pom:4.1-SNAPSHOT
+10:179:pom:4.1-SNAPSHOT
+10:347:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c8d192e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.0-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:177:pom:4.0-SNAPSHOT
+10:178:pom:4.0-SNAPSHOT
+10:179:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f80defb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_347_4.1-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:177:pom:4.1-SNAPSHOT
+10:178:pom:4.1-SNAPSHOT
+10:179:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..dd095bd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.0-SNAPSHOT.ini
@@ -0,0 +1,43 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:338:pom:2.8-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:350:pom:4.0-SNAPSHOT
+10:205:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:261:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..3d5edfe
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_349_4.1-SNAPSHOT.ini
@@ -0,0 +1,44 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:338:pom:3.0-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:350:pom:4.1-SNAPSHOT
+10:205:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:261:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..209ff91
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.0-SNAPSHOT.ini
@@ -0,0 +1,26 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:339:pom:8.0.1p5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:172:pom:4.0-SNAPSHOT
+10:349:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e4c847d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_350_4.1-SNAPSHOT.ini
@@ -0,0 +1,28 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:339:pom:8.0.1p5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:172:pom:4.1-SNAPSHOT
+10:349:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..8282eae
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.0-SNAPSHOT.ini
@@ -0,0 +1,46 @@
+[dependencies]
+1:352:pom:6.1-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:353:pom:1.2.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:248:pom:5.5-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:354:pom:1.6.2-SNAPSHOT
+1:355:pom:3.0.5-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:267:pom:3.0-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:344:pom:1.6.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+10:173:pom:4.0-SNAPSHOT
+10:189:pom:4.0-SNAPSHOT
+10:202:pom:4.0-SNAPSHOT
+10:174:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:331:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..eb1cfa4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_351_4.1-SNAPSHOT.ini
@@ -0,0 +1,50 @@
+[dependencies]
+1:352:pom:6.1-SNAPSHOT
+1:358:pom:1.1.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:353:pom:1.2.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:248:pom:5.5-SNAPSHOT
+1:50:pom:3.4.1-SNAPSHOT
+1:354:pom:1.6.2-SNAPSHOT
+1:355:pom:3.0.5-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:267:pom:3.0-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+1:344:pom:1.6.0-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:289:pom:2.2.0-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+10:173:pom:4.1-SNAPSHOT
+10:189:pom:4.1-SNAPSHOT
+10:202:pom:4.1-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:331:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:363:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..67ffd1f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.0-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..67ffd1f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_356_4.1-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_357_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_357_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..c0c2600
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_357_4.1-SNAPSHOT.ini
@@ -0,0 +1,36 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:4:pom:2.5.4-SNAPSHOT
+1:3:pom:1.28-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:4.2.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_359_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_359_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..5afdd88
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_359_4.1-SNAPSHOT.ini
@@ -0,0 +1,29 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:119:pom:1.0-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:357:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:163:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+1:32:pom:720-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:51:pom:1.6.2-SNAPSHOT
+1:151:pom:3.7.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_360_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_360_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a96c5f9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_360_1.0-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:184:pom:20080807-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:30:pom:0.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_361_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_361_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f6f14ff
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_361_4.1-SNAPSHOT.ini
@@ -0,0 +1,62 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:248:pom:5.5-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:222:pom:beta8-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:72:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:224:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:362:pom:4.1-SNAPSHOT
+10:252:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:216:pom:4.1-SNAPSHOT
+10:230:pom:4.1-SNAPSHOT
+10:220:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_362_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_362_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..2990a16
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_362_4.1-SNAPSHOT.ini
@@ -0,0 +1,30 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:245:pom:1.0.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:254:pom:4.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:245:pom:1.0.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:250:pom:1.0_sap.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:361:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_363_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_363_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d82b427
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_363_4.1-SNAPSHOT.ini
@@ -0,0 +1,10 @@
+[dependencies]
+1:358:pom:1.1.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:40:pom:6.1-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_364_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_364_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..6063aa1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_364_4.1-SNAPSHOT.ini
@@ -0,0 +1,17 @@
+[dependencies]
+10:152:pom:4.1-SNAPSHOT
+1:358:pom:1.1.2-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_366_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_366_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..9f6d56d
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_366_4.1-SNAPSHOT.ini
@@ -0,0 +1,39 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:46:pom:1.0.2-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:185:pom:1.0_sap.2-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:218:pom:2.2.1-SNAPSHOT
+1:219:pom:2.3.4-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:316:pom:4.1-SNAPSHOT
+10:226:pom:4.1-SNAPSHOT
+10:231:pom:4.1-SNAPSHOT
+10:232:pom:4.1-SNAPSHOT
+10:285:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:238:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:228:pom:1.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a5ece58
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.0-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..4f3a993
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_42_4.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..62d0952
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.0-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:49:pom:6.0.5.25-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:52:pom:1.8-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:54:pom:0.1.36-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:60:pom:0.2.9-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:162:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:356:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1feac7e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_43_4.1-SNAPSHOT.ini
@@ -0,0 +1,35 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:48:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:49:pom:6.0.5.25-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:53:pom:2.3.0.677-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:54:pom:0.1.36-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:56:pom:1.2-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:59:pom:1.0.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:60:pom:0.2.9-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:162:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:356:pom:4.1-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..bf91527
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.0-SNAPSHOT.ini
@@ -0,0 +1,46 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:11.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:72:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:242:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+10:155:pom:4.0-SNAPSHOT
+10:121:pom:3.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1010296
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_62_4.1-SNAPSHOT.ini
@@ -0,0 +1,51 @@
+[dependencies]
+1:358:pom:1.1.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:260:pom:2.3.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:69:pom:11.1.0-SNAPSHOT
+1:69:pom:8.1.7-SNAPSHOT
+1:69:pom:9.0.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:72:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:242:pom:4.1-SNAPSHOT
+10:364:pom:4.1-SNAPSHOT
+10:363:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+10:155:pom:4.1-SNAPSHOT
+10:121:pom:3.1-SNAPSHOT
+10:359:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a5831b1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.0-SNAPSHOT.ini
@@ -0,0 +1,68 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:55:pom:4.4-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:79:pom:0.7.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:80:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:251:pom:4.0-SNAPSHOT
+10:180:pom:3.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:156:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:233:pom:4.0-SNAPSHOT
+10:257:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:160:pom:4.0-SNAPSHOT
+10:43:pom:4.0-SNAPSHOT
+10:161:pom:4.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+10:318:pom:4.0-SNAPSHOT
+10:320:pom:4.0-SNAPSHOT
+10:319:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..e7c458a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_72_4.1-SNAPSHOT.ini
@@ -0,0 +1,73 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:63:pom:1.1-SNAPSHOT
+1:63:pom:1.3-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:73:pom:2.4.1-SNAPSHOT
+1:64:pom:1.3-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:65:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:47:pom:2.6.2-SNAPSHOT
+1:76:pom:1.0-SNAPSHOT
+1:66:pom:0.11-SNAPSHOT
+1:67:pom:1.6.5-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:158:pom:3.5-SNAPSHOT
+1:52:pom:2.5-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:68:pom:3.8.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:77:pom:1.45-SNAPSHOT
+1:217:pom:1.0_sap.1-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:78:pom:2.6-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:69:pom:10.1.0-SNAPSHOT
+1:79:pom:0.7.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:70:pom:9.0-SNAPSHOT
+1:29:pom:3.0.5-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:61:pom:1.1-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:80:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:251:pom:4.1-SNAPSHOT
+10:180:pom:3.2-SNAPSHOT
+10:174:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:156:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:233:pom:4.1-SNAPSHOT
+10:257:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:160:pom:4.1-SNAPSHOT
+10:43:pom:4.1-SNAPSHOT
+10:161:pom:4.1-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+10:318:pom:4.1-SNAPSHOT
+10:320:pom:4.1-SNAPSHOT
+10:319:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..b9edb46
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.0-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:81:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:235:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f7660bf
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_80_4.1-SNAPSHOT.ini
@@ -0,0 +1,19 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:81:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:235:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c3bd01f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.0-SNAPSHOT.ini
@@ -0,0 +1,78 @@
+[dependencies]
+1:82:pom:9.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:85:pom:6.4-SNAPSHOT
+1:85:pom:9.53.busObj.CR.1-SNAPSHOT
+1:86:pom:7.13.2-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:87:pom:6b-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1-SNAPSHOT
+1:88:pom:3.5-SNAPSHOT
+1:91:pom:1.5-SNAPSHOT
+1:92:pom:1.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:94:pom:6.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:95:pom:7.0-SNAPSHOT
+1:95:pom:7.1-SNAPSHOT
+1:95:pom:8.0-SNAPSHOT
+1:96:pom:8.0-SNAPSHOT
+1:97:pom:3.0-SNAPSHOT
+1:98:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:99:pom:1.0-SNAPSHOT
+1:100:pom:4.1-SNAPSHOT
+1:101:pom:1.0_sap.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:102:pom:1.busObj.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:106:pom:3.5.5-SNAPSHOT
+1:107:pom:1.0-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:110:pom:4.0-SNAPSHOT
+1:111:pom:1.0-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+1:113:pom:2.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:114:pom:2.50.16.busObj.1-SNAPSHOT
+1:114:pom:2.50.28_sap.1-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.0-SNAPSHOT
+10:117:pom:4.0-SNAPSHOT
+10:139:pom:3.0-SNAPSHOT
+10:62:pom:4.0-SNAPSHOT
+10:163:pom:4.0-SNAPSHOT
+10:246:pom:4.0-SNAPSHOT
+10:167:pom:4.0-SNAPSHOT
+10:351:pom:4.0-SNAPSHOT
+10:247:pom:4.0-SNAPSHOT
+10:225:pom:4.0-SNAPSHOT
+10:207:pom:4.0-SNAPSHOT
+10:243:pom:4.0-SNAPSHOT
+10:252:pom:4.0-SNAPSHOT
+10:266:pom:4.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:141:pom:4.0-SNAPSHOT
+10:145:pom:4.0-SNAPSHOT
+10:152:pom:4.0-SNAPSHOT
+10:315:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
+10:146:pom:4.0-SNAPSHOT
+10:147:pom:4.0-SNAPSHOT
+10:148:pom:4.0-SNAPSHOT
+10:149:pom:4.0-SNAPSHOT
+10:162:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..cdaa48e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_81_4.1-SNAPSHOT.ini
@@ -0,0 +1,78 @@
+[dependencies]
+1:82:pom:9.0-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:44:pom:1.3-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:83:pom:1.10.0-SNAPSHOT
+1:8:pom:2.1.0-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:85:pom:6.4-SNAPSHOT
+1:85:pom:9.53.busObj.CR.1-SNAPSHOT
+1:86:pom:7.13.2-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:87:pom:6b-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:88:pom:3.5-SNAPSHOT
+1:91:pom:1.5-SNAPSHOT
+1:92:pom:1.0-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:93:pom:10.0-SNAPSHOT
+1:94:pom:6.0-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:95:pom:7.0-SNAPSHOT
+1:95:pom:7.1-SNAPSHOT
+1:95:pom:8.0-SNAPSHOT
+1:96:pom:8.0-SNAPSHOT
+1:97:pom:3.0-SNAPSHOT
+1:98:pom:6.0-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:99:pom:1.0-SNAPSHOT
+1:100:pom:4.1-SNAPSHOT
+1:101:pom:1.0_sap.1-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:102:pom:1.busObj.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:103:pom:6403-SNAPSHOT
+1:104:pom:70-SNAPSHOT
+1:105:pom:70-SNAPSHOT
+1:106:pom:3.5.5-SNAPSHOT
+1:107:pom:1.0-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:110:pom:4.0-SNAPSHOT
+1:111:pom:1.0-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+1:113:pom:2.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:114:pom:2.50.16.busObj.1-SNAPSHOT
+1:114:pom:2.50.28_sap.1-SNAPSHOT
+1:116:pom:4.0-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:117:pom:4.1-SNAPSHOT
+10:139:pom:3.1-SNAPSHOT
+10:62:pom:4.1-SNAPSHOT
+10:163:pom:4.1-SNAPSHOT
+10:246:pom:4.1-SNAPSHOT
+10:167:pom:4.1-SNAPSHOT
+10:351:pom:4.1-SNAPSHOT
+10:247:pom:4.1-SNAPSHOT
+10:225:pom:4.1-SNAPSHOT
+10:207:pom:4.1-SNAPSHOT
+10:243:pom:4.1-SNAPSHOT
+10:252:pom:4.1-SNAPSHOT
+10:266:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
+10:141:pom:4.1-SNAPSHOT
+10:145:pom:4.1-SNAPSHOT
+10:152:pom:4.1-SNAPSHOT
+10:315:pom:4.1-SNAPSHOT
+10:12:pom:4.1-SNAPSHOT
+10:146:pom:4.1-SNAPSHOT
+10:147:pom:4.1-SNAPSHOT
+10:148:pom:4.1-SNAPSHOT
+10:149:pom:4.1-SNAPSHOT
+10:162:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_90_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_90_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/10_90_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_100_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_100_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_100_4.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_101_1.0_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_101_1.0_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_101_1.0_sap.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_102_1.busObj.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_102_1.busObj.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_102_1.busObj.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_103_6403-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_103_6403-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_103_6403-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_104_70-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_104_70-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_104_70-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_105_70-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_105_70-SNAPSHOT.ini
new file mode 100644
index 0000000..f334a37
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_105_70-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_106_3.5.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_106_3.5.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_106_3.5.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_107_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_107_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_107_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_108_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_108_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..949516a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_108_4.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:109:pom:6.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_109_6.0.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_109_6.0.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_109_6.0.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_110_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_110_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d33fdc7
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_110_4.0-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:27:pom:6.0-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_111_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_111_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..ef8aa98
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_111_1.0-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:18:pom:5.1.1.41-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_112_1.0.30-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_112_1.0.30-SNAPSHOT.ini
new file mode 100644
index 0000000..d1717bb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_112_1.0.30-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_113_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_113_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..663a1b8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_113_2.0-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:87:pom:6b-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:106:pom:3.5.5-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.16.busObj.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.16.busObj.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b80aa77
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.16.busObj.1-SNAPSHOT.ini
@@ -0,0 +1,12 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:108:pom:4.0-SNAPSHOT
+1:110:pom:4.0-SNAPSHOT
+1:112:pom:1.0.30-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.28_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.28_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..d986d0c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_114_2.50.28_sap.1-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:115:pom:4.2.1-SNAPSHOT
+1:114:pom:2.50.16.busObj.1-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_115_4.2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_115_4.2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_115_4.2.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_116_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_116_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..235f139
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_116_4.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:18:pom:5.1.1.41-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.2.2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.2.2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..debc3bf
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.2.2.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.5.3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.5.3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_118_10.5.3.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_119_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_119_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..fe9ba3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_119_1.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:7:pom:5.8.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_120_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_120_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..ba84e97
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_120_1.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_122_3.1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_122_3.1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_122_3.1.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_123_3.1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_123_3.1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_123_3.1.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_124_2.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_124_2.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_124_2.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_8.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_8.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_8.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_125_9.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.81-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.81-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.81-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.90-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.90-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_2.90-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_3.50-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_3.50-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_126_3.50-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.20-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.20-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.20-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.30-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.30-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_127_6.30-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_128_2006-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_128_2006-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_128_2006-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_129_10.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_129_10.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_129_10.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_10.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_11.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_11.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_11.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_9.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_9.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_130_9.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_5.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_131_6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_132_7.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_132_7.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_132_7.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_12.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_12.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_12.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_133_15.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.6-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.6-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.6-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_134_12.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_10.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_10.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_10.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_11.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_11.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_135_11.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.04-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.04-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.04-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.06-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.06-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_136_3.06-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_137_2.2.12-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_137_2.2.12-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_137_2.2.12-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_138_7.7.06-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_138_7.7.06-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_138_7.7.06-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_13_1.7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_13_1.7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_13_1.7.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_5.5.28-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_5.5.28-SNAPSHOT.ini
new file mode 100644
index 0000000..05b1294
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_5.5.28-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.18-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.18-SNAPSHOT.ini
new file mode 100644
index 0000000..5ebd8f8
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.18-SNAPSHOT.ini
@@ -0,0 +1,14 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.5-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.24-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.24-SNAPSHOT.ini
new file mode 100644
index 0000000..816c494
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_140_6.0.24-SNAPSHOT.ini
@@ -0,0 +1,14 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:14:pom:2.5.2-SNAPSHOT
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_142_9.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_142_9.1-SNAPSHOT.ini
new file mode 100644
index 0000000..08ea238
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_142_9.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:77:pom:1.45-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_143_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_143_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_143_6.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_144_15.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_144_15.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a9482de
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_144_15.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:69:pom:8.1.7-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_14_2.5.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_14_2.5.2-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_14_2.5.2-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_151_3.7.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_151_3.7.1-SNAPSHOT.ini
new file mode 100644
index 0000000..ccd5f42
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_151_3.7.1-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_153_1.1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_153_1.1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_153_1.1.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_154_3.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_154_3.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_154_3.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_157_0.0.356_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_157_0.0.356_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_157_0.0.356_sap.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_158_3.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_158_3.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_158_3.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_159_2.1_03-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_159_2.1_03-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_159_2.1_03-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_15_1.36.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_15_1.36.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_15_1.36.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_11.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_11.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_11.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_7.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_9.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_9.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_165_9.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_166_720-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_166_720-SNAPSHOT.ini
new file mode 100644
index 0000000..f2d19b9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_166_720-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_168_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_168_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_168_6.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_169_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_169_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_169_4.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_16_1.8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_16_1.8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_16_1.8.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_170_10.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_170_10.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_170_10.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.45-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.45-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.45-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.46-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.46-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_171_8.46-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_17_1.2.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_17_1.2.3-SNAPSHOT.ini
new file mode 100644
index 0000000..8f93c82
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_17_1.2.3-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_181_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_181_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..5185fdb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_181_1.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:182:pom:3.2-SNAPSHOT
+1:13:pom:1.7.0-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_182_3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_182_3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_182_3.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_183_0.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_183_0.9-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_183_0.9-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_184_20080807-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_184_20080807-SNAPSHOT.ini
new file mode 100644
index 0000000..05b1294
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_184_20080807-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b9f2329
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.1-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:140:pom:5.5.28-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.2-SNAPSHOT.ini
new file mode 100644
index 0000000..58c3546
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_185_1.0_sap.2-SNAPSHOT.ini
@@ -0,0 +1,15 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:45:pom:3.1-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:140:pom:6.0.24-SNAPSHOT
+1:140:pom:5.5.28-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.5-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_186_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_186_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_186_4.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_187_4.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_187_4.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_187_4.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_188_1.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_188_1.9-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_188_1.9-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1.41-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1.41-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_18_5.1.1.41-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_190_2.0.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_190_2.0.8-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_190_2.0.8-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_191_3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_191_3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_191_3.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_192_1.8.0.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_192_1.8.0.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_192_1.8.0.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_193_0.8.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_193_0.8.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_193_0.8.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_194_2.3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_194_2.3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_194_2.3.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_195_3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_195_3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_195_3.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_196_1.2.12-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_196_1.2.12-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_196_1.2.12-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_197_5.1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_197_5.1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_197_5.1.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_198_9.1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_198_9.1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_198_9.1.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_199_2.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_199_2.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_199_2.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_19_6.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_19_6.3-SNAPSHOT.ini
new file mode 100644
index 0000000..fd9cb65
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_19_6.3-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_3.1.12-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_3.1.12-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_3.1.12-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_5.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_5.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_200_5.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_201_3.5.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_201_3.5.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_201_3.5.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_204_1.6.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_204_1.6.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_204_1.6.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_208_10.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_208_10.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f334a37
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_208_10.0-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_209_3.51-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_209_3.51-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_209_3.51-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_20_3.3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_20_3.3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..7ee3cc0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_20_3.3.2-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:18:pom:5.1.1.41-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:21:pom:3.2.1.2-SNAPSHOT
+10:22:pom:4.1-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_211_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_211_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c0a2de3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_211_1.0-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:25:pom:0.86-beta1-SNAPSHOT
+1:20:pom:3.3.2-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:19:pom:6.3-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_217_1.0_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_217_1.0_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..056fb8a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_217_1.0_sap.1-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_218_2.2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_218_2.2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..62af74c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_218_2.2.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:68:pom:3.8.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_219_2.3.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_219_2.3.4-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_219_2.3.4-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_21_3.2.1.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_21_3.2.1.2-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_21_3.2.1.2-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_221_8.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_221_8.9-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_221_8.9-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_1.1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_1.1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_1.1.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_beta8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_beta8-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_222_beta8-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_223_7.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_223_7.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_223_7.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_227_7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_227_7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_227_7.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_234_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_234_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_234_1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_239_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_239_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..c151d03
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_239_3.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.5-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_23_1.2.6_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_23_1.2.6_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..76cd884
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_23_1.2.6_sap.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_240_720-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_240_720-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_240_720-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_244_1.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_244_1.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_244_1.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_245_1.0.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_245_1.0.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_245_1.0.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_248_5.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_248_5.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_248_5.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_249_9.2.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_249_9.2.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_249_9.2.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_24_1.2.10-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_24_1.2.10-SNAPSHOT.ini
new file mode 100644
index 0000000..c0c19c1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_24_1.2.10-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:25:pom:0.86-beta1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_250_1.0_sap.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_250_1.0_sap.1-SNAPSHOT.ini
new file mode 100644
index 0000000..b2947dd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_250_1.0_sap.1-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:23:pom:1.2.6_sap.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:55:pom:4.8.2-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_253_1.3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_253_1.3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_253_1.3.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_256_3.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_256_3.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_256_3.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_259_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_259_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_259_2.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_25_0.86-beta1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_25_0.86-beta1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_25_0.86-beta1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.2.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.2.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.2.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.3.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.3.3-SNAPSHOT.ini
new file mode 100644
index 0000000..c22cffc
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_260_2.3.3-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:42:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_264_6.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_264_6.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_264_6.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_267_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_267_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..003ec27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_267_3.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.0.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.0.1-SNAPSHOT.ini
new file mode 100644
index 0000000..8cd878a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.0.1-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.8.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.8.1-SNAPSHOT.ini
new file mode 100644
index 0000000..24e6ee2
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_3.8.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:26:pom:3.0.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_4.2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_4.2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_26_4.2.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_270_1.6.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_270_1.6.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_270_1.6.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_273_1.0.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_273_1.0.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_273_1.0.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_274_1.0.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_274_1.0.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_274_1.0.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_278_1.8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_278_1.8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_278_1.8.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_27_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_27_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_27_6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_282_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_282_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_282_2.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_283_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_283_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_283_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.1.2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.1.2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.1.2.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_287_1.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_289_2.2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_289_2.2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..778341f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_289_2.2.0-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_28_4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_28_4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_28_4.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_290_8.0.2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_290_8.0.2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_290_8.0.2.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_293_0.2.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_293_0.2.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_293_0.2.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_296_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_296_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_296_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_299_7.20-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_299_7.20-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_299_7.20-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_29_3.0.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_29_3.0.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_29_3.0.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_2_5.50-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_2_5.50-SNAPSHOT.ini
new file mode 100644
index 0000000..5da39fa
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_2_5.50-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:3:pom:1.28-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_300_7.1.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_300_7.1.8-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_300_7.1.8-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_302_1.015-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_302_1.015-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_302_1.015-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_304_9.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_304_9.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_304_9.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_308_1.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_308_1.8-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_308_1.8-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_309_1.15.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_309_1.15.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_309_1.15.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_30_0.7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_30_0.7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_30_0.7.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_310_4.2.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_310_4.2.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_310_4.2.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_311_2.3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_311_2.3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..003ec27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_311_2.3.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_312_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_312_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_312_4.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_313_2.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_313_2.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_313_2.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_314_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_314_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..056fb8a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_314_6.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_31_2.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_31_2.2-SNAPSHOT.ini
new file mode 100644
index 0000000..7efd19e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_31_2.2-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_323_11.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_323_11.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_323_11.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_324_5.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_324_5.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_324_5.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_326_0.9.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_326_0.9.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_326_0.9.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_32_720-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_32_720-SNAPSHOT.ini
new file mode 100644
index 0000000..b266200
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_32_720-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:33:pom:711-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_2.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_2.8-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_2.8-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..685c7b4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_338_3.0-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_339_8.0.1p5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_339_8.0.1p5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_339_8.0.1p5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_33_711-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_33_711-SNAPSHOT.ini
new file mode 100644
index 0000000..003ec27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_33_711-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.11-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.11-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.11-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.9-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_340_2.9-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_0.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_0.9-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_0.9-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_341_1.3-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_342_1.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_342_1.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_342_1.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_344_1.6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_344_1.6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_344_1.6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_348_3.8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_348_3.8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_348_3.8.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_34_1.13-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_34_1.13-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_34_1.13-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_352_6.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_352_6.1-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_352_6.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_353_1.2.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_353_1.2.3-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_353_1.2.3-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_354_1.6.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_354_1.6.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_354_1.6.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_355_3.0.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_355_3.0.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_355_3.0.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_358_1.1.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_358_1.1.2-SNAPSHOT.ini
new file mode 100644
index 0000000..0b6a100
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_358_1.1.2-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_35_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_35_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_35_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_365_3.4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_365_3.4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..8df1fc0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_365_3.4.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_367_8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_367_8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..f2d19b9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_367_8.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_368_4.7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_368_4.7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_368_4.7.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_369_2.4.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_369_2.4.5-SNAPSHOT.ini
new file mode 100644
index 0000000..eba920b
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_369_2.4.5-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:368:pom:4.7.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_36_2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_36_2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_36_2.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_370_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_370_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_370_6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_371_822-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_371_822-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_371_822-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_37_5.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_37_5.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_37_5.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.5-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.5-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.6-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.6-SNAPSHOT.ini
new file mode 100644
index 0000000..2eb1d30
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_38_1.3.6-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:39:pom:0.9.8l-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_39_0.9.8l-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_39_0.9.8l-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_39_0.9.8l-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_3_1.28-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_3_1.28-SNAPSHOT.ini
new file mode 100644
index 0000000..0060172
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_3_1.28-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:4:pom:2.5.4-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_40_6.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_40_6.1-SNAPSHOT.ini
new file mode 100644
index 0000000..003ec27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_40_6.1-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_41_5.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_41_5.0-SNAPSHOT.ini
new file mode 100644
index 0000000..abd2445
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_41_5.0-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:15:pom:1.36.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:38:pom:1.3.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
+10:42:pom:4.0-SNAPSHOT
+10:12:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.3-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.4-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_44_1.4-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_45_3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_45_3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..1080940
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_45_3.1-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:46:pom:1.0.2-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_46_1.0.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_46_1.0.2-SNAPSHOT.ini
new file mode 100644
index 0000000..e84e949
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_46_1.0.2-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:47:pom:2.6.2-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.6.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.6.2-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.6.2-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.9.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.9.1-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_47_2.9.1-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_48_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_48_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_48_1.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_49_6.0.5.25-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_49_6.0.5.25-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_49_6.0.5.25-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_4_2.5.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_4_2.5.4-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_4_2.5.4-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.3-SNAPSHOT.ini
new file mode 100644
index 0000000..fb4de50
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.3-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:51:pom:1.6.2-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4-SNAPSHOT.ini
new file mode 100644
index 0000000..8df1fc0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..aec8335
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.4.1-SNAPSHOT.ini
@@ -0,0 +1,3 @@
+[dependencies]
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.5-SNAPSHOT.ini
new file mode 100644
index 0000000..115875c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_50_3.5-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_51_1.6.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_51_1.6.2-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_51_1.6.2-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_1.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_1.8-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_1.8-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_2.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_2.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_52_2.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_53_2.3.0.677-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_53_2.3.0.677-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_53_2.3.0.677-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_54_0.1.36-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_54_0.1.36-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_54_0.1.36-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_3.8.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_3.8.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_3.8.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.4-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.4-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.8.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.8.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_55_4.8.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_56_1.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_56_1.2-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_56_1.2-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_57_4.0.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_57_4.0.5-SNAPSHOT.ini
new file mode 100644
index 0000000..25a47d6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_57_4.0.5-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:58:pom:2.4.7-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:28:pom:4.1-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.1-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_58_2.4.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_58_2.4.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_58_2.4.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_59_1.0.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_59_1.0.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_59_1.0.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.5-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.5-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.6-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.6-SNAPSHOT.ini
new file mode 100644
index 0000000..0e6ed3a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_5_1.6-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_60_0.2.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_60_0.2.9-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_60_0.2.9-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..cff07d1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.1-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.2.12-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.2.12-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_61_1.2.12-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.1-SNAPSHOT.ini
new file mode 100644
index 0000000..45e08ca
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:5:pom:1.6-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_63_1.3-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_64_1.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_64_1.3-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_64_1.3-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_65_2.1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_65_2.1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_65_2.1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_66_0.11-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_66_0.11-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_66_0.11-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_67_1.6.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_67_1.6.5-SNAPSHOT.ini
new file mode 100644
index 0000000..ae14f69
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_67_1.6.5-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_68_3.8.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_68_3.8.1-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_68_3.8.1-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_10.1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_10.1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..659afdd
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_10.1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:41:pom:5.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_11.1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_11.1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_11.1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_8.1.7-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_8.1.7-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_8.1.7-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_9.0.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_9.0.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_69_9.0.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_6_1.5.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_6_1.5.8-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_6_1.5.8-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_70_9.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_70_9.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_70_9.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.3.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.3.8-SNAPSHOT.ini
new file mode 100644
index 0000000..704ed7e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.3.8-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.4.C-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.4.C-SNAPSHOT.ini
new file mode 100644
index 0000000..704ed7e
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_71_1.1.4.C-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:57:pom:4.0.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..80e54a1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.3.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:74:pom:3.2.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.4.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.4.1-SNAPSHOT.ini
new file mode 100644
index 0000000..5ed05de
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_73_2.4.1-SNAPSHOT.ini
@@ -0,0 +1,13 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:74:pom:3.5.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:31:pom:2.2-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:5:pom:1.6-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.2.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.2.0-SNAPSHOT.ini
new file mode 100644
index 0000000..d50887f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.2.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:25-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.5.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.5.0-SNAPSHOT.ini
new file mode 100644
index 0000000..a789cb4
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_74_3.5.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:75:pom:26-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_25-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_25-SNAPSHOT.ini
new file mode 100644
index 0000000..a27924c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_25-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_26-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_26-SNAPSHOT.ini
new file mode 100644
index 0000000..a27924c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_75_26-SNAPSHOT.ini
@@ -0,0 +1,11 @@
+[dependencies]
+1:13:pom:1.7.0-SNAPSHOT
+1:50:pom:3.3-SNAPSHOT
+1:50:pom:3.4-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:5:pom:1.5-SNAPSHOT
+1:71:pom:1.1.3.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_76_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_76_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_76_1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_77_1.45-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_77_1.45-SNAPSHOT.ini
new file mode 100644
index 0000000..07dd311
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_77_1.45-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:45:pom:3.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_78_2.6-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_78_2.6-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_78_2.6-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_79_0.7.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_79_0.7.2-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_79_0.7.2-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.8-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.8-SNAPSHOT.ini
new file mode 100644
index 0000000..c41e8d3
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.8-SNAPSHOT.ini
@@ -0,0 +1,8 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:37:pom:5.5-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.9-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.9-SNAPSHOT.ini
new file mode 100644
index 0000000..96ea704
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_7_5.8.9-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:17:pom:1.2.3-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:36:pom:2.0-SNAPSHOT
+1:37:pom:5.5-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_82_9.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_82_9.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_82_9.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_83_1.10.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_83_1.10.0-SNAPSHOT.ini
new file mode 100644
index 0000000..af47d8a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_83_1.10.0-SNAPSHOT.ini
@@ -0,0 +1,9 @@
+[dependencies]
+1:8:pom:2.7.0-SNAPSHOT
+1:26:pom:3.0.1-SNAPSHOT
+1:2:pom:5.50-SNAPSHOT
+1:84:pom:2.2.0036-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_84_2.2.0036-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_84_2.2.0036-SNAPSHOT.ini
new file mode 100644
index 0000000..99dfc84
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_84_2.2.0036-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:6:pom:1.5.8-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_6.4-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_6.4-SNAPSHOT.ini
new file mode 100644
index 0000000..9ec9fb5
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_6.4-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:45:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f2d19b9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.1-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.53.busObj.CR.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.53.busObj.CR.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_85_9.53.busObj.CR.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_86_7.13.2-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_86_7.13.2-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_86_7.13.2-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_87_6b-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_87_6b-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_87_6b-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_88_3.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_88_3.5-SNAPSHOT.ini
new file mode 100644
index 0000000..f2ae278
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_88_3.5-SNAPSHOT.ini
@@ -0,0 +1,7 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+1:89:pom:2.3-SNAPSHOT
+10:90:pom:1.0-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_89_2.3-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_89_2.3-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_89_2.3-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_8_2.7.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_91_1.5-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_91_1.5-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_91_1.5-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_92_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_92_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_92_1.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_93_10.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_93_10.0-SNAPSHOT.ini
new file mode 100644
index 0000000..003ec27
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_93_10.0-SNAPSHOT.ini
@@ -0,0 +1,5 @@
+[dependencies]
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_4.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_4.0-SNAPSHOT.ini
new file mode 100644
index 0000000..5941ff1
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_4.0-SNAPSHOT.ini
@@ -0,0 +1,2 @@
+[dependencies]
+1:94:pom:6.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_94_6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.1-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_7.1-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_95_8.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_96_8.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_96_8.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_96_8.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_97_3.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_97_3.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_97_3.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_98_6.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_98_6.0-SNAPSHOT.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_98_6.0-SNAPSHOT.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_99_1.0-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_99_1.0-SNAPSHOT.ini
new file mode 100644
index 0000000..678ce19
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_99_1.0-SNAPSHOT.ini
@@ -0,0 +1,6 @@
+[dependencies]
+1:2:pom:5.50-SNAPSHOT
+1:9:pom:3.1-SNAPSHOT
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_9_3.1-SNAPSHOT.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_9_3.1-SNAPSHOT.ini
new file mode 100644
index 0000000..f334a37
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle-big/1_9_3.1-SNAPSHOT.ini
@@ -0,0 +1,4 @@
+[dependencies]
+1:6:pom:1.5.8-SNAPSHOT
+1:7:pom:5.8.8-SNAPSHOT
+10:11:pom:4.0-SNAPSHOT
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle.txt
new file mode 100644
index 0000000..5496316
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle.txt
@@ -0,0 +1,9 @@
+cycle:root:jar:1
++- cycle:a:jar:1 compile          (a)
+|  \- cycle:b:jar:1 compile
+|     \- cycle:c:jar:1 compile
+|        \- ^a
+\- cycle:b:jar:1 compile          (b)
+   \- cycle:c:jar:1 compile
+      \- cycle:a:jar:1 compile
+         \- ^b
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_a_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_a_1.ini
new file mode 100644
index 0000000..e1228ce
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_a_1.ini
@@ -0,0 +1,2 @@
+[dependencies]
+cycle:b:jar:1
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_b_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_b_1.ini
new file mode 100644
index 0000000..408f1a0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_b_1.ini
@@ -0,0 +1,2 @@
+[dependencies]
+cycle:c:jar:1
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_c_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_c_1.ini
new file mode 100644
index 0000000..18d989a
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_c_1.ini
@@ -0,0 +1,2 @@
+[dependencies]
+cycle:a:jar:1
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_root_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_root_1.ini
new file mode 100644
index 0000000..33d2bd6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/cycle_root_1.ini
@@ -0,0 +1,3 @@
+[dependencies]
+cycle:a:jar:1
+cycle:b:jar:1
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/duplicate_transitive_dependency.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/duplicate_transitive_dependency.ini
new file mode 100644
index 0000000..09be313
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/duplicate_transitive_dependency.ini
@@ -0,0 +1,3 @@
+[dependencies]
+gid:aid:ext:ver
+gid:aid2:ext:ver
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedPartialSubtreeOnError.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedPartialSubtreeOnError.txt
new file mode 100644
index 0000000..6ef2faf
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedPartialSubtreeOnError.txt
@@ -0,0 +1,6 @@
+subtree:comparison:ext:error
++- duplicate:transitive:ext:dependency compile
+|  +- gid:aid:ext:ver compile
+|  |  \- gid:aid2:ext:ver compile
+|  \- gid:aid2:ext:ver compile
+\- git:aid:ext:ver compile
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedSubtreeComparisonResult.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedSubtreeComparisonResult.txt
new file mode 100644
index 0000000..63e1318
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/expectedSubtreeComparisonResult.txt
@@ -0,0 +1,7 @@
+subtree:comparison:ext:ver
++- duplicate:transitive:ext:dependency compile
+|  +- gid:aid:ext:ver compile
+|  |  \- gid:aid2:ext:ver compile
+|  \- gid:aid2:ext:ver compile
+\- gid:aid:ext:ver compile
+   \- gid:aid2:ext:ver compile
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_9.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_9.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_9.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_managedVersion.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_managedVersion.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_managedVersion.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_ver.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid2_ver.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_1.ini
new file mode 100644
index 0000000..69b3f85
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_1.ini
@@ -0,0 +1,2 @@
+[dependencies]
+gid:aid2:ext:[1,9]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_ver.ini
new file mode 100644
index 0000000..b5aac5f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/gid_aid_ver.ini
@@ -0,0 +1,2 @@
+[dependencies]
+gid:aid2:ext:ver
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/duplicate_transitive_managed.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/duplicate_transitive_managed.ini
new file mode 100644
index 0000000..09be313
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/duplicate_transitive_managed.ini
@@ -0,0 +1,3 @@
+[dependencies]
+gid:aid:ext:ver
+gid:aid2:ext:ver
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid2_managed.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid2_managed.ini
new file mode 100644
index 0000000..61a252c
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid2_managed.ini
@@ -0,0 +1 @@
+[dependencies]
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid_ver.ini
new file mode 100644
index 0000000..b5aac5f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/gid_aid_ver.ini
@@ -0,0 +1,2 @@
+[dependencies]
+gid:aid2:ext:ver
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/subtree_comparison_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/subtree_comparison_ver.ini
new file mode 100644
index 0000000..5b1bcc9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed/subtree_comparison_ver.ini
@@ -0,0 +1,3 @@
+[dependencies]
+duplicate:transitive:ext:dependency
+gid:aid:ext:ver
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/managed_aid_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed_aid_ver.ini
new file mode 100644
index 0000000..6095505
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/managed_aid_ver.ini
@@ -0,0 +1,5 @@
+[dependencies]
+gid:aid:ext:ver
+
+[manageddependencies]
+gid:aid2:ext:managedVersion:managedScope
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/missing_description_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/missing_description_ver.ini
new file mode 100644
index 0000000..91b0c20
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/missing_description_ver.ini
@@ -0,0 +1,2 @@
+[dependencies]
+missing:artifact:file:description
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_error.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_error.ini
new file mode 100644
index 0000000..76d2ba6
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_error.ini
@@ -0,0 +1,3 @@
+[dependencies]
+duplicate:transitive:ext:dependency
+git:aid:ext:ver
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_ver.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_ver.ini
new file mode 100644
index 0000000..5b1bcc9
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/subtree_comparison_ver.ini
@@ -0,0 +1,3 @@
+[dependencies]
+duplicate:transitive:ext:dependency
+gid:aid:ext:ver
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_1.ini
new file mode 100644
index 0000000..cfdbaeb
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_1.ini
@@ -0,0 +1,2 @@
+[dependencies]
+test:b:jar:1
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_2.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_2.ini
new file mode 100644
index 0000000..0117b0f
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_a_2.ini
@@ -0,0 +1,2 @@
+[dependencies]
+test:b:jar:2
diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_b_2.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_b_2.ini
new file mode 100644
index 0000000..04a3ff0
--- /dev/null
+++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/versionless-cycle/test_b_2.ini
@@ -0,0 +1,2 @@
+[dependencies]
+test:a:jar:1
diff --git a/maven-resolver-spi/pom.xml b/maven-resolver-spi/pom.xml
new file mode 100644
index 0000000..a978a2e
--- /dev/null
+++ b/maven-resolver-spi/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-spi</artifactId>
+
+  <name>Maven Artifact Resolver SPI</name>
+  <description>
+    The service provider interface for repository system implementations and repository connectors.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.spi</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java
new file mode 100644
index 0000000..3d82ac1
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java
@@ -0,0 +1,264 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A download of an artifact from a remote repository. A repository connector processing this download has to use
+ * {@link #setException(ArtifactTransferException)} and {@link #setSupportedContexts(Collection)} (if applicable) to
+ * report the results of the transfer.
+ */
+public final class ArtifactDownload
+    extends ArtifactTransfer
+{
+
+    private boolean existenceCheck;
+
+    private String checksumPolicy = "";
+
+    private String context = "";
+
+    private Collection<String> contexts;
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    /**
+     * Creates a new uninitialized download.
+     */
+    public ArtifactDownload()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a new download with the specified properties.
+     * 
+     * @param artifact The artifact to download, may be {@code null}.
+     * @param context The context in which this download is performed, may be {@code null}.
+     * @param file The local file to download the artifact to, may be {@code null}.
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     */
+    public ArtifactDownload( Artifact artifact, String context, File file, String checksumPolicy )
+    {
+        setArtifact( artifact );
+        setRequestContext( context );
+        setFile( file );
+        setChecksumPolicy( checksumPolicy );
+    }
+
+    @Override
+    public ArtifactDownload setArtifact( Artifact artifact )
+    {
+        super.setArtifact( artifact );
+        return this;
+    }
+
+    /**
+     * {@inheritDoc} <em>Note:</em> In case of {@link #isExistenceCheck()}, this method may return {@code null}.
+     */
+    @Override
+    public File getFile()
+    {
+        return super.getFile();
+    }
+
+    @Override
+    public ArtifactDownload setFile( File file )
+    {
+        super.setFile( file );
+        return this;
+    }
+
+    /**
+     * Indicates whether this transfer shall only verify the existence of the artifact in the remote repository rather
+     * than actually downloading the file. Just like with an actual transfer, a connector is expected to signal the
+     * non-existence of the artifact by associating an {@link org.eclipse.aether.transfer.ArtifactNotFoundException
+     * ArtifactNotFoundException} with this download. <em>Note:</em> If an existence check is requested,
+     * {@link #getFile()} may be {@code null}, i.e. the connector must not try to access the local file.
+     * 
+     * @return {@code true} if only the artifact existence shall be verified, {@code false} to actually download the
+     *         artifact.
+     */
+    public boolean isExistenceCheck()
+    {
+        return existenceCheck;
+    }
+
+    /**
+     * Controls whether this transfer shall only verify the existence of the artifact in the remote repository rather
+     * than actually downloading the file.
+     * 
+     * @param existenceCheck {@code true} if only the artifact existence shall be verified, {@code false} to actually
+     *            download the artifact.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactDownload setExistenceCheck( boolean existenceCheck )
+    {
+        this.existenceCheck = existenceCheck;
+        return this;
+    }
+
+    /**
+     * Gets the checksum policy for this transfer.
+     * 
+     * @return The checksum policy, never {@code null}.
+     */
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    /**
+     * Sets the checksum policy for this transfer.
+     * 
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactDownload setChecksumPolicy( String checksumPolicy )
+    {
+        this.checksumPolicy = ( checksumPolicy != null ) ? checksumPolicy : "";
+        return this;
+    }
+
+    /**
+     * Gets the context of this transfer.
+     * 
+     * @return The context id, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the context of this transfer.
+     * 
+     * @param context The context id, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactDownload setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the set of request contexts in which the artifact is generally available. Repository managers can indicate
+     * that an artifact is available in more than the requested context to avoid future remote trips for the same
+     * artifact in a different context.
+     * 
+     * @return The set of requests context in which the artifact is available, never {@code null}.
+     */
+    public Collection<String> getSupportedContexts()
+    {
+        return ( contexts != null ) ? contexts : Collections.singleton( context );
+    }
+
+    /**
+     * Sets the set of request contexts in which the artifact is generally available. Repository managers can indicate
+     * that an artifact is available in more than the requested context to avoid future remote trips for the same
+     * artifact in a different context. The set of supported contexts defaults to the original request context if not
+     * overridden by the repository connector.
+     * 
+     * @param contexts The set of requests context in which the artifact is available, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactDownload setSupportedContexts( Collection<String> contexts )
+    {
+        if ( contexts == null || contexts.isEmpty() )
+        {
+            this.contexts = Collections.singleton( context );
+        }
+        else
+        {
+            this.contexts = contexts;
+        }
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories that are being aggregated by the physically contacted remote repository (i.e. a
+     * repository manager).
+     * 
+     * @return The remote repositories being aggregated, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories that are being aggregated by the physically contacted remote repository (i.e. a
+     * repository manager).
+     * 
+     * @param repositories The remote repositories being aggregated, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactDownload setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    @Override
+    public ArtifactDownload setException( ArtifactTransferException exception )
+    {
+        super.setException( exception );
+        return this;
+    }
+
+    @Override
+    public ArtifactDownload setListener( TransferListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    @Override
+    public ArtifactDownload setTrace( RequestTrace trace )
+    {
+        super.setTrace( trace );
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " - " + ( isExistenceCheck() ? "?" : "" ) + getFile();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java
new file mode 100644
index 0000000..8399512
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+
+/**
+ * A download/upload of an artifact.
+ * 
+ * @noextend This class is not intended to be extended by clients.
+ */
+public abstract class ArtifactTransfer
+    extends Transfer
+{
+
+    private Artifact artifact;
+
+    private File file;
+
+    private ArtifactTransferException exception;
+
+    ArtifactTransfer()
+    {
+        // hide
+    }
+
+    /**
+     * Gets the artifact being transferred.
+     * 
+     * @return The artifact being transferred or {@code null} if not set.
+     */
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    /**
+     * Sets the artifact to transfer.
+     * 
+     * @param artifact The artifact, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactTransfer setArtifact( Artifact artifact )
+    {
+        this.artifact = artifact;
+        return this;
+    }
+
+    /**
+     * Gets the local file the artifact is downloaded to or uploaded from. In case of a download, a connector should
+     * first transfer the bytes to a temporary file and only overwrite the target file once the entire download is
+     * completed such that an interrupted/failed download does not corrupt the current file contents.
+     * 
+     * @return The local file or {@code null} if not set.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the local file the artifact is downloaded to or uploaded from.
+     * 
+     * @param file The local file, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactTransfer setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * Gets the exception that occurred during the transfer (if any).
+     * 
+     * @return The exception or {@code null} if the transfer was successful.
+     */
+    public ArtifactTransferException getException()
+    {
+        return exception;
+    }
+
+    /**
+     * Sets the exception that occurred during the transfer.
+     * 
+     * @param exception The exception, may be {@code null} to denote a successful transfer.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public ArtifactTransfer setException( ArtifactTransferException exception )
+    {
+        this.exception = exception;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
new file mode 100644
index 0000000..f85539e
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
@@ -0,0 +1,98 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * An upload of an artifact to a remote repository. A repository connector processing this upload has to use
+ * {@link #setException(ArtifactTransferException)} to report the results of the transfer.
+ */
+public final class ArtifactUpload
+    extends ArtifactTransfer
+{
+
+    /**
+     * Creates a new uninitialized upload.
+     */
+    public ArtifactUpload()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a new upload with the specified properties.
+     * 
+     * @param artifact The artifact to upload, may be {@code null}.
+     * @param file The local file to upload the artifact from, may be {@code null}.
+     */
+    public ArtifactUpload( Artifact artifact, File file )
+    {
+        setArtifact( artifact );
+        setFile( file );
+    }
+
+    @Override
+    public ArtifactUpload setArtifact( Artifact artifact )
+    {
+        super.setArtifact( artifact );
+        return this;
+    }
+
+    @Override
+    public ArtifactUpload setFile( File file )
+    {
+        super.setFile( file );
+        return this;
+    }
+
+    @Override
+    public ArtifactUpload setException( ArtifactTransferException exception )
+    {
+        super.setException( exception );
+        return this;
+    }
+
+    @Override
+    public ArtifactUpload setListener( TransferListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    @Override
+    public ArtifactUpload setTrace( RequestTrace trace )
+    {
+        super.setTrace( trace );
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getArtifact() + " - " + getFile();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java
new file mode 100644
index 0000000..be3a2d0
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java
@@ -0,0 +1,186 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A download of metadata from a remote repository. A repository connector processing this download has to use
+ * {@link #setException(MetadataTransferException)} to report the results of the transfer.
+ */
+public final class MetadataDownload
+    extends MetadataTransfer
+{
+
+    private String checksumPolicy = "";
+
+    private String context = "";
+
+    private List<RemoteRepository> repositories = Collections.emptyList();
+
+    /**
+     * Creates a new uninitialized download.
+     */
+    public MetadataDownload()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a new download with the specified properties.
+     * 
+     * @param metadata The metadata to download, may be {@code null}.
+     * @param context The context in which this download is performed, may be {@code null}.
+     * @param file The local file to download the metadata to, may be {@code null}.
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     */
+    public MetadataDownload( Metadata metadata, String context, File file, String checksumPolicy )
+    {
+        setMetadata( metadata );
+        setFile( file );
+        setChecksumPolicy( checksumPolicy );
+        setRequestContext( context );
+    }
+
+    @Override
+    public MetadataDownload setMetadata( Metadata metadata )
+    {
+        super.setMetadata( metadata );
+        return this;
+    }
+
+    @Override
+    public MetadataDownload setFile( File file )
+    {
+        super.setFile( file );
+        return this;
+    }
+
+    /**
+     * Gets the checksum policy for this transfer.
+     * 
+     * @return The checksum policy, never {@code null}.
+     */
+    public String getChecksumPolicy()
+    {
+        return checksumPolicy;
+    }
+
+    /**
+     * Sets the checksum policy for this transfer.
+     * 
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataDownload setChecksumPolicy( String checksumPolicy )
+    {
+        this.checksumPolicy = ( checksumPolicy != null ) ? checksumPolicy : "";
+        return this;
+    }
+
+    /**
+     * Gets the context of this transfer.
+     * 
+     * @return The context id, never {@code null}.
+     */
+    public String getRequestContext()
+    {
+        return context;
+    }
+
+    /**
+     * Sets the request context of this transfer.
+     * 
+     * @param context The context id, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataDownload setRequestContext( String context )
+    {
+        this.context = ( context != null ) ? context : "";
+        return this;
+    }
+
+    /**
+     * Gets the remote repositories that are being aggregated by the physically contacted remote repository (i.e. a
+     * repository manager).
+     * 
+     * @return The remote repositories being aggregated, never {@code null}.
+     */
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    /**
+     * Sets the remote repositories that are being aggregated by the physically contacted remote repository (i.e. a
+     * repository manager).
+     * 
+     * @param repositories The remote repositories being aggregated, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataDownload setRepositories( List<RemoteRepository> repositories )
+    {
+        if ( repositories == null )
+        {
+            this.repositories = Collections.emptyList();
+        }
+        else
+        {
+            this.repositories = repositories;
+        }
+        return this;
+    }
+
+    @Override
+    public MetadataDownload setException( MetadataTransferException exception )
+    {
+        super.setException( exception );
+        return this;
+    }
+
+    @Override
+    public MetadataDownload setListener( TransferListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    @Override
+    public MetadataDownload setTrace( RequestTrace trace )
+    {
+        super.setTrace( trace );
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " - " + getFile();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java
new file mode 100644
index 0000000..94eb46e
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.MetadataTransferException;
+
+/**
+ * A download/upload of metadata.
+ * 
+ * @noextend This class is not intended to be extended by clients.
+ */
+public abstract class MetadataTransfer
+    extends Transfer
+{
+
+    private Metadata metadata;
+
+    private File file;
+
+    private MetadataTransferException exception;
+
+    MetadataTransfer()
+    {
+        // hide
+    }
+
+    /**
+     * Gets the metadata being transferred.
+     * 
+     * @return The metadata being transferred or {@code null} if not set.
+     */
+    public Metadata getMetadata()
+    {
+        return metadata;
+    }
+
+    /**
+     * Sets the metadata to transfer.
+     * 
+     * @param metadata The metadata, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataTransfer setMetadata( Metadata metadata )
+    {
+        this.metadata = metadata;
+        return this;
+    }
+
+    /**
+     * Gets the local file the metadata is downloaded to or uploaded from. In case of a download, a connector should
+     * first transfer the bytes to a temporary file and only overwrite the target file once the entire download is
+     * completed such that an interrupted/failed download does not corrupt the current file contents.
+     * 
+     * @return The local file or {@code null} if not set.
+     */
+    public File getFile()
+    {
+        return file;
+    }
+
+    /**
+     * Sets the local file the metadata is downloaded to or uploaded from.
+     * 
+     * @param file The local file, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataTransfer setFile( File file )
+    {
+        this.file = file;
+        return this;
+    }
+
+    /**
+     * Gets the exception that occurred during the transfer (if any).
+     * 
+     * @return The exception or {@code null} if the transfer was successful.
+     */
+    public MetadataTransferException getException()
+    {
+        return exception;
+    }
+
+    /**
+     * Sets the exception that occurred during the transfer.
+     * 
+     * @param exception The exception, may be {@code null} to denote a successful transfer.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    public MetadataTransfer setException( MetadataTransferException exception )
+    {
+        this.exception = exception;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java
new file mode 100644
index 0000000..d992757
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java
@@ -0,0 +1,98 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * An upload of metadata to a remote repository. A repository connector processing this upload has to use
+ * {@link #setException(MetadataTransferException)} to report the results of the transfer.
+ */
+public final class MetadataUpload
+    extends MetadataTransfer
+{
+
+    /**
+     * Creates a new uninitialized upload.
+     */
+    public MetadataUpload()
+    {
+        // enables default constructor
+    }
+
+    /**
+     * Creates a new upload with the specified properties.
+     * 
+     * @param metadata The metadata to upload, may be {@code null}.
+     * @param file The local file to upload the metadata from, may be {@code null}.
+     */
+    public MetadataUpload( Metadata metadata, File file )
+    {
+        setMetadata( metadata );
+        setFile( file );
+    }
+
+    @Override
+    public MetadataUpload setMetadata( Metadata metadata )
+    {
+        super.setMetadata( metadata );
+        return this;
+    }
+
+    @Override
+    public MetadataUpload setFile( File file )
+    {
+        super.setFile( file );
+        return this;
+    }
+
+    @Override
+    public MetadataUpload setException( MetadataTransferException exception )
+    {
+        super.setException( exception );
+        return this;
+    }
+
+    @Override
+    public MetadataUpload setListener( TransferListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    @Override
+    public MetadataUpload setTrace( RequestTrace trace )
+    {
+        super.setTrace( trace );
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return getMetadata() + " - " + getFile();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnector.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnector.java
new file mode 100644
index 0000000..51e0627
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnector.java
@@ -0,0 +1,77 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.util.Collection;
+
+/**
+ * A connector for a remote repository. The connector is responsible for downloading/uploading of artifacts and metadata
+ * from/to a remote repository.
+ * <p>
+ * If applicable, a connector should obey connect/request timeouts and other relevant settings from the
+ * {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties() configuration properties} of the repository
+ * session it has been obtained for. However, a connector must not emit any events to the transfer listener configured
+ * for the session. Instead, transfer events must be emitted only to the listener (if any) specified for a given
+ * download/upload request.
+ * <p>
+ * <strong>Note:</strong> While a connector itself can use multiple threads internally to performs the transfers,
+ * clients must not call a connector concurrently, i.e. connectors are generally not thread-safe.
+ * 
+ * @see org.eclipse.aether.spi.connector.transport.TransporterProvider
+ * @see org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider
+ * @see org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider
+ */
+public interface RepositoryConnector
+    extends Closeable
+{
+
+    /**
+     * Performs the specified downloads. If a download fails, the connector stores the underlying exception in the
+     * download object such that callers can inspect the result via {@link ArtifactDownload#getException()} and
+     * {@link MetadataDownload#getException()}, respectively. If reasonable, a connector should continue to process the
+     * remaining downloads after an error to retrieve as many items as possible. The connector may perform the transfers
+     * concurrently and in any order.
+     * 
+     * @param artifactDownloads The artifact downloads to perform, may be {@code null} or empty.
+     * @param metadataDownloads The metadata downloads to perform, may be {@code null} or empty.
+     */
+    void get( Collection<? extends ArtifactDownload> artifactDownloads,
+              Collection<? extends MetadataDownload> metadataDownloads );
+
+    /**
+     * Performs the specified uploads. If an upload fails, the connector stores the underlying exception in the upload
+     * object such that callers can inspect the result via {@link ArtifactUpload#getException()} and
+     * {@link MetadataUpload#getException()}, respectively. The connector may perform the transfers concurrently and in
+     * any order.
+     * 
+     * @param artifactUploads The artifact uploads to perform, may be {@code null} or empty.
+     * @param metadataUploads The metadata uploads to perform, may be {@code null} or empty.
+     */
+    void put( Collection<? extends ArtifactUpload> artifactUploads, Collection<? extends MetadataUpload> metadataUploads );
+
+    /**
+     * Closes this connector and frees any network resources associated with it. Once closed, a connector must not be
+     * used for further transfers, any attempt to do so would yield a {@link IllegalStateException} or similar. Closing
+     * an already closed connector is harmless and has no effect.
+     */
+    void close();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnectorFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnectorFactory.java
new file mode 100644
index 0000000..0d401c4
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/RepositoryConnectorFactory.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.NoRepositoryConnectorException;
+
+/**
+ * A factory to create repository connectors. A repository connector is responsible for uploads/downloads to/from a
+ * certain kind of remote repository. When the repository system needs a repository connector for a given remote
+ * repository, it iterates the registered factories in descending order of their priority and calls
+ * {@link #newInstance(RepositorySystemSession, RemoteRepository)} on them. The first connector returned by a factory
+ * will then be used for the transfer.
+ */
+public interface RepositoryConnectorFactory
+{
+
+    /**
+     * Tries to create a repository connector for the specified remote repository. Typically, a factory will inspect
+     * {@link RemoteRepository#getProtocol()} and {@link RemoteRepository#getContentType()} to determine whether it can
+     * handle a repository.
+     * 
+     * @param session The repository system session from which to configure the connector, must not be {@code null}. In
+     *            particular, a connector must notify any {@link RepositorySystemSession#getTransferListener()} set for
+     *            the session and should obey the timeouts configured for the session.
+     * @param repository The remote repository to create a connector for, must not be {@code null}.
+     * @return The connector for the given repository, never {@code null}.
+     * @throws NoRepositoryConnectorException If the factory cannot create a connector for the specified remote
+     *             repository.
+     */
+    RepositoryConnector newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryConnectorException;
+
+    /**
+     * The priority of this factory. When multiple factories can handle a given repository, factories with higher
+     * priority are preferred over those with lower priority.
+     * 
+     * @return The priority of this factory.
+     */
+    float getPriority();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/Transfer.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/Transfer.java
new file mode 100644
index 0000000..fc77011
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/Transfer.java
@@ -0,0 +1,93 @@
+package org.eclipse.aether.spi.connector;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RequestTrace;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * An artifact/metadata transfer.
+ * 
+ * @noextend This class is not intended to be extended by clients.
+ */
+public abstract class Transfer
+{
+
+    private TransferListener listener;
+
+    private RequestTrace trace;
+
+    Transfer()
+    {
+        // hide from public
+    }
+
+    /**
+     * Gets the exception that occurred during the transfer (if any).
+     * 
+     * @return The exception or {@code null} if the transfer was successful.
+     */
+    public abstract Exception getException();
+
+    /**
+     * Gets the listener that is to be notified during the transfer.
+     * 
+     * @return The transfer listener or {@code null} if none.
+     */
+    public TransferListener getListener()
+    {
+        return listener;
+    }
+
+    /**
+     * Sets the listener that is to be notified during the transfer.
+     * 
+     * @param listener The transfer listener to notify, may be {@code null} if none.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    Transfer setListener( TransferListener listener )
+    {
+        this.listener = listener;
+        return this;
+    }
+
+    /**
+     * Gets the trace information that describes the higher level request/operation in which this transfer is issued.
+     * 
+     * @return The trace information about the higher level operation or {@code null} if none.
+     */
+    public RequestTrace getTrace()
+    {
+        return trace;
+    }
+
+    /**
+     * Sets the trace information that describes the higher level request/operation in which this transfer is issued.
+     * 
+     * @param trace The trace information about the higher level operation, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     */
+    Transfer setTrace( RequestTrace trace )
+    {
+        this.trace = trace;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicy.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicy.java
new file mode 100644
index 0000000..eb1716d
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicy.java
@@ -0,0 +1,142 @@
+package org.eclipse.aether.spi.connector.checksum;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.transfer.ChecksumFailureException;
+
+/**
+ * A checksum policy gets employed by repository connectors to validate the integrity of a downloaded file. For each
+ * downloaded file, a checksum policy instance is obtained and presented with the available checksums to conclude
+ * whether the download is valid or not. The following pseudo-code illustrates the usage of a checksum policy by a
+ * repository connector in some more detail (the retry logic has been omitted for the sake of brevity):
+ * 
+ * <pre>
+ * void validateChecksums() throws ChecksumFailureException {
+ *   for (checksum : checksums) {
+ *     switch (checksum.state) {
+ *       case MATCH:
+ *         if (policy.onChecksumMatch(...)) {
+ *           return;
+ *         }
+ *         break;
+ *       case MISMATCH:
+ *         policy.onChecksumMismatch(...);
+ *         break;
+ *       case ERROR:
+ *         policy.onChecksumError(...);
+ *         break;
+ *     }
+ *   }
+ *   policy.onNoMoreChecksums();
+ * }
+ * 
+ * void downloadFile() throws Exception {
+ *   ...
+ *   policy = newChecksumPolicy();
+ *   try {
+ *     validateChecksums();
+ *   } catch (ChecksumFailureException e) {
+ *     if (!policy.onTransferChecksumFailure(...)) {
+ *       throw e;
+ *     }
+ *   }
+ * }
+ * </pre>
+ * 
+ * Checksum policies might be stateful and are generally not thread-safe.
+ */
+public interface ChecksumPolicy
+{
+
+    /**
+     * Bit flag indicating a checksum which is not part of the official repository layout/structure.
+     */
+    int KIND_UNOFFICIAL = 0x01;
+
+    /**
+     * Signals a match between the locally computed checksum value and the checksum value declared by the remote
+     * repository.
+     * 
+     * @param algorithm The name of the checksum algorithm being used, must not be {@code null}.
+     * @param kind A bit field providing further details about the checksum. See the {@code KIND_*} constants in this
+     *            interface for possible bit flags.
+     * @return {@code true} to accept the download as valid and stop further validation, {@code false} to continue
+     *         validation with the next checksum.
+     */
+    boolean onChecksumMatch( String algorithm, int kind );
+
+    /**
+     * Signals a mismatch between the locally computed checksum value and the checksum value declared by the remote
+     * repository. A simple policy would just rethrow the provided exception. More sophisticated policies could update
+     * their internal state and defer a conclusion until all available checksums have been processed.
+     * 
+     * @param algorithm The name of the checksum algorithm being used, must not be {@code null}.
+     * @param kind A bit field providing further details about the checksum. See the {@code KIND_*} constants in this
+     *            interface for possible bit flags.
+     * @param exception The exception describing the checksum mismatch, must not be {@code null}.
+     * @throws ChecksumFailureException If the checksum validation is to be failed. If the method returns normally,
+     *             validation continues with the next checksum.
+     */
+    void onChecksumMismatch( String algorithm, int kind, ChecksumFailureException exception )
+        throws ChecksumFailureException;
+
+    /**
+     * Signals an error while computing the local checksum value or retrieving the checksum value from the remote
+     * repository.
+     * 
+     * @param algorithm The name of the checksum algorithm being used, must not be {@code null}.
+     * @param kind A bit field providing further details about the checksum. See the {@code KIND_*} constants in this
+     *            interface for possible bit flags.
+     * @param exception The exception describing the checksum error, must not be {@code null}.
+     * @throws ChecksumFailureException If the checksum validation is to be failed. If the method returns normally,
+     *             validation continues with the next checksum.
+     */
+    void onChecksumError( String algorithm, int kind, ChecksumFailureException exception )
+        throws ChecksumFailureException;
+
+    /**
+     * Signals that all available checksums have been processed.
+     * 
+     * @throws ChecksumFailureException If the checksum validation is to be failed. If the method returns normally, the
+     *             download is assumed to be valid.
+     */
+    void onNoMoreChecksums()
+        throws ChecksumFailureException;
+
+    /**
+     * Signals that the download is being retried after a previously thrown {@link ChecksumFailureException} that is
+     * {@link ChecksumFailureException#isRetryWorthy() retry-worthy}. Policies that maintain internal state will usually
+     * have to reset some of this state at this point to prepare for a new round of validation.
+     */
+    void onTransferRetry();
+
+    /**
+     * Signals that (even after a potential retry) checksum validation has failed. A policy could opt to merely log this
+     * issue or insist on rejecting the downloaded file as unusable.
+     * 
+     * @param exception The exception that was thrown from a prior call to
+     *            {@link #onChecksumMismatch(String, int, ChecksumFailureException)},
+     *            {@link #onChecksumError(String, int, ChecksumFailureException)} or {@link #onNoMoreChecksums()}.
+     * @return {@code true} to accept the download nevertheless and let artifact resolution succeed, {@code false} to
+     *         reject the transferred file as unusable.
+     */
+    boolean onTransferChecksumFailure( ChecksumFailureException exception );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicyProvider.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicyProvider.java
new file mode 100644
index 0000000..f502300
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumPolicyProvider.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.spi.connector.checksum;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.TransferResource;
+
+/**
+ * Assists repository connectors in applying checksum policies to downloaded resources.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface ChecksumPolicyProvider
+{
+
+    /**
+     * Retrieves the checksum policy with the specified identifier for use on the given remote resource.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param repository The repository hosting the resource being transferred, must not be {@code null}.
+     * @param resource The transfer resource on which the policy will be applied, must not be {@code null}.
+     * @param policy The identifier of the policy to apply, must not be {@code null}.
+     * @return The policy to apply or {@code null} if checksums should be ignored.
+     */
+    ChecksumPolicy newChecksumPolicy( RepositorySystemSession session, RemoteRepository repository,
+                                      TransferResource resource, String policy );
+
+    /**
+     * Returns the least strict policy. A checksum policy is said to be less strict than another policy if it would
+     * accept a downloaded resource in all cases where the other policy would reject the resource.
+     * 
+     * @param session The repository system session during which the request is made, must not be {@code null}.
+     * @param policy1 A policy to compare, must not be {@code null}.
+     * @param policy2 A policy to compare, must not be {@code null}.
+     * @return The least strict policy among the two input policies.
+     */
+    String getEffectiveChecksumPolicy( RepositorySystemSession session, String policy1, String policy2 );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/package-info.java
new file mode 100644
index 0000000..94d0653
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/package-info.java
@@ -0,0 +1,25 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The support infrastructure for repository connectors to apply checksum policies when validating the integrity of
+ * downloaded files.  
+ */
+package org.eclipse.aether.spi.connector.checksum;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayout.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayout.java
new file mode 100644
index 0000000..a36c542
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayout.java
@@ -0,0 +1,180 @@
+package org.eclipse.aether.spi.connector.layout;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import java.util.List;
+import java.util.Locale;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * The layout for a remote repository whose artifacts/metadata can be addressed via URIs.
+ * <p>
+ * <strong>Note:</strong> Implementations must be stateless.
+ */
+public interface RepositoryLayout
+{
+
+    /**
+     * A descriptor for a checksum file. This descriptor simply associates the location of a checksum file with the
+     * underlying algorithm used to calculate/verify it. Checksum algorithms are denoted by names as used with
+     * {@link java.security.MessageDigest#getInstance(String)}, e.g. {@code "SHA-1"} or {@code "MD5"}.
+     */
+    static final class Checksum
+    {
+
+        private final String algorithm;
+
+        private final URI location;
+
+        /**
+         * Creates a new checksum file descriptor with the specified algorithm and location. The method
+         * {@link #forLocation(URI, String)} is usually more convenient though.
+         * 
+         * @param algorithm The algorithm used to calculate the checksum, must not be {@code null}.
+         * @param location The relative URI to the checksum file within a repository, must not be {@code null}.
+         */
+        public Checksum( String algorithm, URI location )
+        {
+            verify( algorithm, location );
+            this.algorithm = algorithm;
+            this.location = location;
+        }
+
+        /**
+         * Creates a checksum file descriptor for the specified artifact/metadata location and algorithm. The location
+         * of the checksum file itself is derived from the supplied resource URI by appending the file extension
+         * corresponding to the algorithm. The file extension in turn is derived from the algorithm name by stripping
+         * out any hyphen ('-') characters and lower-casing the name, e.g. "SHA-1" is mapped to ".sha1".
+         * 
+         * @param location The relative URI to the artifact/metadata whose checksum file is being obtained, must not be
+         *            {@code null} and must not have a query or fragment part.
+         * @param algorithm The algorithm used to calculate the checksum, must not be {@code null}.
+         * @return The checksum file descriptor, never {@code null}.
+         */
+        public static Checksum forLocation( URI location, String algorithm )
+        {
+            verify( algorithm, location );
+            if ( location.getRawQuery() != null )
+            {
+                throw new IllegalArgumentException( "resource location must not have query parameters: " + location );
+            }
+            if ( location.getRawFragment() != null )
+            {
+                throw new IllegalArgumentException( "resource location must not have a fragment: " + location );
+            }
+            String extension = '.' + algorithm.replace( "-", "" ).toLowerCase( Locale.ENGLISH );
+            return new Checksum( algorithm, URI.create( location.toString() + extension ) );
+        }
+
+        private static void verify( String algorithm, URI location )
+        {
+            requireNonNull( algorithm, "checksum algorithm cannot be null" );
+            if ( algorithm.length() == 0 )
+            {
+                throw new IllegalArgumentException( "checksum algorithm cannot be empty" );
+            }
+            requireNonNull( location, "checksum location cannot be null" );
+            if ( location.isAbsolute() )
+            {
+                throw new IllegalArgumentException( "checksum location must be relative" );
+            }
+        }
+
+        /**
+         * Gets the name of the algorithm that is used to calculate the checksum.
+         * 
+         * @return The algorithm name, never {@code null}.
+         * @see java.security.MessageDigest#getInstance(String)
+         */
+        public String getAlgorithm()
+        {
+            return algorithm;
+        }
+
+        /**
+         * Gets the location of the checksum file with a remote repository. The URI is relative to the root directory of
+         * the repository.
+         * 
+         * @return The relative URI to the checksum file, never {@code null}.
+         */
+        public URI getLocation()
+        {
+            return location;
+        }
+
+        @Override
+        public String toString()
+        {
+            return location + " (" + algorithm + ")";
+        }
+
+    }
+
+    /**
+     * Gets the location within a remote repository where the specified artifact resides. The URI is relative to the
+     * root directory of the repository.
+     * 
+     * @param artifact The artifact to get the URI for, must not be {@code null}.
+     * @param upload {@code false} if the artifact is being downloaded, {@code true} if the artifact is being uploaded.
+     * @return The relative URI to the artifact, never {@code null}.
+     */
+    URI getLocation( Artifact artifact, boolean upload );
+
+    /**
+     * Gets the location within a remote repository where the specified metadata resides. The URI is relative to the
+     * root directory of the repository.
+     * 
+     * @param metadata The metadata to get the URI for, must not be {@code null}.
+     * @param upload {@code false} if the metadata is being downloaded, {@code true} if the metadata is being uploaded.
+     * @return The relative URI to the metadata, never {@code null}.
+     */
+    URI getLocation( Metadata metadata, boolean upload );
+
+    /**
+     * Gets the checksums files that a remote repository keeps to help detect data corruption during transfers of the
+     * specified artifact.
+     * 
+     * @param artifact The artifact to get the checksum files for, must not be {@code null}.
+     * @param upload {@code false} if the checksums are being downloaded/verified, {@code true} if the checksums are
+     *            being uploaded/created.
+     * @param location The relative URI to the artifact within the repository as previously obtained from
+     *            {@link #getLocation(Artifact, boolean)}, must not be {@code null}.
+     * @return The checksum files for the given artifact, possibly empty but never {@code null}.
+     */
+    List<Checksum> getChecksums( Artifact artifact, boolean upload, URI location );
+
+    /**
+     * Gets the checksums files that a remote repository keeps to help detect data corruption during transfers of the
+     * specified metadata.
+     * 
+     * @param metadata The metadata to get the checksum files for, must not be {@code null}.
+     * @param upload {@code false} if the checksums are being downloaded/verified, {@code true} if the checksums are
+     *            being uploaded/created.
+     * @param location The relative URI to the metadata within the repository as previously obtained from
+     *            {@link #getLocation(Metadata, boolean)}, must not be {@code null}.
+     * @return The checksum files for the given metadata, possibly empty but never {@code null}.
+     */
+    List<Checksum> getChecksums( Metadata metadata, boolean upload, URI location );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutFactory.java
new file mode 100644
index 0000000..8aa71d7
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutFactory.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.spi.connector.layout;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+
+/**
+ * A factory to obtain repository layouts. A repository layout is responsible to map an artifact or some metadata to a
+ * URI relative to the repository root where the resource resides. When the repository system needs to access a given
+ * remote repository, it iterates the registered factories in descending order of their priority and calls
+ * {@link #newInstance(RepositorySystemSession, RemoteRepository)} on them. The first layout returned by a factory will
+ * then be used for transferring artifacts/metadata.
+ */
+public interface RepositoryLayoutFactory
+{
+
+    /**
+     * Tries to create a repository layout for the specified remote repository. Typically, a factory will inspect
+     * {@link RemoteRepository#getContentType()} to determine whether it can handle a repository.
+     * 
+     * @param session The repository system session from which to configure the layout, must not be {@code null}.
+     * @param repository The remote repository to create a layout for, must not be {@code null}.
+     * @return The layout for the given repository, never {@code null}.
+     * @throws NoRepositoryLayoutException If the factory cannot create a repository layout for the specified remote
+     *             repository.
+     */
+    RepositoryLayout newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryLayoutException;
+
+    /**
+     * The priority of this factory. When multiple factories can handle a given repository, factories with higher
+     * priority are preferred over those with lower priority.
+     * 
+     * @return The priority of this factory.
+     */
+    float getPriority();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutProvider.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutProvider.java
new file mode 100644
index 0000000..5cdf53b
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/RepositoryLayoutProvider.java
@@ -0,0 +1,47 @@
+package org.eclipse.aether.spi.connector.layout;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.NoRepositoryLayoutException;
+
+/**
+ * Retrieves a repository layout from the installed layout factories.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface RepositoryLayoutProvider
+{
+
+    /**
+     * Tries to retrieve a repository layout for the specified remote repository.
+     * 
+     * @param session The repository system session from which to configure the layout, must not be {@code null}.
+     * @param repository The remote repository to create a layout for, must not be {@code null}.
+     * @return The layout for the given repository, never {@code null}.
+     * @throws NoRepositoryLayoutException If none of the installed layout factories can provide a repository layout for
+     *             the specified remote repository.
+     */
+    RepositoryLayout newRepositoryLayout( RepositorySystemSession session, RemoteRepository repository )
+        throws NoRepositoryLayoutException;
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/package-info.java
new file mode 100644
index 0000000..2b36f7c
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/layout/package-info.java
@@ -0,0 +1,26 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The contract to locate URI-based resources using custom repository layouts. By implementing a
+ * {@link org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory} and registering it with the repository
+ * system, an application enables access to remote repositories that use new content types/layouts.  
+ */
+package org.eclipse.aether.spi.connector.layout;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/package-info.java
new file mode 100644
index 0000000..f31a2a8
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/package-info.java
@@ -0,0 +1,30 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The contract to access artifacts/metadata in remote repositories. By implementing a
+ * {@link org.eclipse.aether.spi.connector.RepositoryConnectorFactory} and registering it with the repository system,
+ * an application can enable access to arbitrary remote repositories. It should be noted that a repository connector is
+ * powerful yet burdensome to implement. In many cases, implementing a 
+ * {@link org.eclipse.aether.spi.connector.transport.TransporterFactory} or 
+ * {@link org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory} will be sufficient and easier to access a
+ * custom remote repository.
+ */
+package org.eclipse.aether.spi.connector;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/AbstractTransporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/AbstractTransporter.java
new file mode 100644
index 0000000..21a0da3
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/AbstractTransporter.java
@@ -0,0 +1,260 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+/**
+ * A skeleton implementation for custom transporters.
+ */
+public abstract class AbstractTransporter
+    implements Transporter
+{
+
+    private final AtomicBoolean closed;
+
+    /**
+     * Enables subclassing.
+     */
+    protected AbstractTransporter()
+    {
+        closed = new AtomicBoolean();
+    }
+
+    public void peek( PeekTask task )
+        throws Exception
+    {
+        failIfClosed( task );
+        implPeek( task );
+    }
+
+    /**
+     * Implements {@link #peek(PeekTask)}, gets only called if the transporter has not been closed.
+     * 
+     * @param task The existence check to perform, must not be {@code null}.
+     * @throws Exception If the existence of the specified resource could not be confirmed.
+     */
+    protected abstract void implPeek( PeekTask task )
+        throws Exception;
+
+    public void get( GetTask task )
+        throws Exception
+    {
+        failIfClosed( task );
+        implGet( task );
+    }
+
+    /**
+     * Implements {@link #get(GetTask)}, gets only called if the transporter has not been closed.
+     * 
+     * @param task The download to perform, must not be {@code null}.
+     * @throws Exception If the transfer failed.
+     */
+    protected abstract void implGet( GetTask task )
+        throws Exception;
+
+    /**
+     * Performs stream-based I/O for the specified download task and notifies the configured transport listener.
+     * Subclasses might want to invoke this utility method from within their {@link #implGet(GetTask)} to avoid
+     * boilerplate I/O code.
+     * 
+     * @param task The download to perform, must not be {@code null}.
+     * @param is The input stream to download the data from, must not be {@code null}.
+     * @param close {@code true} if the supplied input stream should be automatically closed, {@code false} to leave the
+     *            stream open.
+     * @param length The size in bytes of the downloaded resource or {@code -1} if unknown, not to be confused with the
+     *            length of the supplied input stream which might be smaller if the download is resumed.
+     * @param resume {@code true} if the download resumes from {@link GetTask#getResumeOffset()}, {@code false} if the
+     *            download starts at the first byte of the resource.
+     * @throws IOException If the transfer encountered an I/O error.
+     * @throws TransferCancelledException If the transfer was cancelled.
+     */
+    protected void utilGet( GetTask task, InputStream is, boolean close, long length, boolean resume )
+        throws IOException, TransferCancelledException
+    {
+        OutputStream os = null;
+        try
+        {
+            os = task.newOutputStream( resume );
+            task.getListener().transportStarted( resume ? task.getResumeOffset() : 0L, length );
+            copy( os, is, task.getListener() );
+            os.close();
+            os = null;
+
+            if ( close )
+            {
+                is.close();
+                is = null;
+            }
+        }
+        finally
+        {
+            try
+            {
+                if ( os != null )
+                {
+                    os.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+            finally
+            {
+                try
+                {
+                    if ( close && is != null )
+                    {
+                        is.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+        }
+    }
+
+    public void put( PutTask task )
+        throws Exception
+    {
+        failIfClosed( task );
+        implPut( task );
+    }
+
+    /**
+     * Implements {@link #put(PutTask)}, gets only called if the transporter has not been closed.
+     * 
+     * @param task The upload to perform, must not be {@code null}.
+     * @throws Exception If the transfer failed.
+     */
+    protected abstract void implPut( PutTask task )
+        throws Exception;
+
+    /**
+     * Performs stream-based I/O for the specified upload task and notifies the configured transport listener.
+     * Subclasses might want to invoke this utility method from within their {@link #implPut(PutTask)} to avoid
+     * boilerplate I/O code.
+     * 
+     * @param task The upload to perform, must not be {@code null}.
+     * @param os The output stream to upload the data to, must not be {@code null}.
+     * @param close {@code true} if the supplied output stream should be automatically closed, {@code false} to leave
+     *            the stream open.
+     * @throws IOException If the transfer encountered an I/O error.
+     * @throws TransferCancelledException If the transfer was cancelled.
+     */
+    protected void utilPut( PutTask task, OutputStream os, boolean close )
+        throws IOException, TransferCancelledException
+    {
+        InputStream is = null;
+        try
+        {
+            task.getListener().transportStarted( 0, task.getDataLength() );
+            is = task.newInputStream();
+            copy( os, is, task.getListener() );
+
+            if ( close )
+            {
+                os.close();
+            }
+            else
+            {
+                os.flush();
+            }
+
+            os = null;
+
+            is.close();
+            is = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( close && os != null )
+                {
+                    os.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+            finally
+            {
+                try
+                {
+                    if ( is != null )
+                    {
+                        is.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+        }
+    }
+
+    public void close()
+    {
+        if ( closed.compareAndSet( false, true ) )
+        {
+            implClose();
+        }
+    }
+
+    /**
+     * Implements {@link #close()}, gets only called if the transporter has not already been closed.
+     */
+    protected abstract void implClose();
+
+    private void failIfClosed( TransportTask task )
+    {
+        if ( closed.get() )
+        {
+            throw new IllegalStateException( "transporter closed, cannot execute task " + task );
+        }
+    }
+
+    private static void copy( OutputStream os, InputStream is, TransportListener listener )
+        throws IOException, TransferCancelledException
+    {
+        ByteBuffer buffer = ByteBuffer.allocate( 1024 * 32 );
+        byte[] array = buffer.array();
+        for ( int read = is.read( array ); read >= 0; read = is.read( array ) )
+        {
+            os.write( array, 0, read );
+            buffer.rewind();
+            buffer.limit( read );
+            listener.transportProgressed( buffer );
+        }
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java
new file mode 100644
index 0000000..dd9c867
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java
@@ -0,0 +1,259 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A task to download a resource from the remote repository.
+ *
+ * @see Transporter#get(GetTask)
+ */
+public final class GetTask
+    extends TransportTask
+{
+
+    private File dataFile;
+
+    private boolean resume;
+
+    private ByteArrayOutputStream dataBytes;
+
+    private Map<String, String> checksums;
+
+    /**
+     * Creates a new task for the specified remote resource.
+     * 
+     * @param location The relative location of the resource in the remote repository, must not be {@code null}.
+     */
+    public GetTask( URI location )
+    {
+        checksums = Collections.emptyMap();
+        setLocation( location );
+    }
+
+    /**
+     * Opens an output stream to store the downloaded data. Depending on {@link #getDataFile()}, this stream writes
+     * either to a file on disk or a growable buffer in memory. It's the responsibility of the caller to close the
+     * provided stream.
+     * 
+     * @return The output stream for the data, never {@code null}. The stream is unbuffered.
+     * @throws IOException If the stream could not be opened.
+     */
+    public OutputStream newOutputStream()
+        throws IOException
+    {
+        return newOutputStream( false );
+    }
+
+    /**
+     * Opens an output stream to store the downloaded data. Depending on {@link #getDataFile()}, this stream writes
+     * either to a file on disk or a growable buffer in memory. It's the responsibility of the caller to close the
+     * provided stream.
+     * 
+     * @param resume {@code true} if the download resumes from the byte offset given by {@link #getResumeOffset()},
+     *            {@code false} if the download starts at the first byte of the resource.
+     * @return The output stream for the data, never {@code null}. The stream is unbuffered.
+     * @throws IOException If the stream could not be opened.
+     */
+    public OutputStream newOutputStream( boolean resume )
+        throws IOException
+    {
+        if ( dataFile != null )
+        {
+            return new FileOutputStream( dataFile, this.resume && resume );
+        }
+        if ( dataBytes == null )
+        {
+            dataBytes = new ByteArrayOutputStream( 1024 );
+        }
+        else if ( !resume )
+        {
+            dataBytes.reset();
+        }
+        return dataBytes;
+    }
+
+    /**
+     * Gets the file (if any) where the downloaded data should be stored. If the specified file already exists, it will
+     * be overwritten.
+     * 
+     * @return The data file or {@code null} if the data will be buffered in memory.
+     */
+    public File getDataFile()
+    {
+        return dataFile;
+    }
+
+    /**
+     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
+     * overwritten. Unless the caller can reasonably expect the resource to be small, use of a data file is strongly
+     * recommended to avoid exhausting heap memory during the download.
+     * 
+     * @param dataFile The file to store the downloaded data, may be {@code null} to store the data in memory.
+     * @return This task for chaining, never {@code null}.
+     */
+    public GetTask setDataFile( File dataFile )
+    {
+        return setDataFile( dataFile, false );
+    }
+
+    /**
+     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
+     * overwritten or appended to, depending on the {@code resume} argument and the capabilities of the transporter.
+     * Unless the caller can reasonably expect the resource to be small, use of a data file is strongly recommended to
+     * avoid exhausting heap memory during the download.
+     * 
+     * @param dataFile The file to store the downloaded data, may be {@code null} to store the data in memory.
+     * @param resume {@code true} to request resuming a previous download attempt, starting from the current length of
+     *            the data file, {@code false} to download the resource from its beginning.
+     * @return This task for chaining, never {@code null}.
+     */
+    public GetTask setDataFile( File dataFile, boolean resume )
+    {
+        this.dataFile = dataFile;
+        this.resume = resume;
+        return this;
+    }
+
+    /**
+     * Gets the byte offset within the resource from which the download should resume if supported.
+     * 
+     * @return The zero-based index of the first byte to download or {@code 0} for a full download from the start of the
+     *         resource, never negative.
+     */
+    public long getResumeOffset()
+    {
+        if ( resume )
+        {
+            if ( dataFile != null )
+            {
+                return dataFile.length();
+            }
+            if ( dataBytes != null )
+            {
+                return dataBytes.size();
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Gets the data that was downloaded into memory. <strong>Note:</strong> This method may only be called if
+     * {@link #getDataFile()} is {@code null} as otherwise the downloaded data has been written directly to disk.
+     * 
+     * @return The possibly empty data bytes, never {@code null}.
+     */
+    public byte[] getDataBytes()
+    {
+        if ( dataFile != null || dataBytes == null )
+        {
+            return EMPTY;
+        }
+        return dataBytes.toByteArray();
+    }
+
+    /**
+     * Gets the data that was downloaded into memory as a string. The downloaded data is assumed to be encoded using
+     * UTF-8. <strong>Note:</strong> This method may only be called if {@link #getDataFile()} is {@code null} as
+     * otherwise the downloaded data has been written directly to disk.
+     * 
+     * @return The possibly empty data string, never {@code null}.
+     */
+    public String getDataString()
+    {
+        if ( dataFile != null || dataBytes == null )
+        {
+            return "";
+        }
+        return new String( dataBytes.toByteArray(), StandardCharsets.UTF_8 );
+    }
+
+    /**
+     * Sets the listener that is to be notified during the transfer.
+     *
+     * @param listener The listener to notify of progress, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    public GetTask setListener( TransportListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    /**
+     * Gets the checksums which the remote repository advertises for the resource. The map is keyed by algorithm name
+     * (cf. {@link java.security.MessageDigest#getInstance(String)}) and the values are hexadecimal representations of
+     * the corresponding value. <em>Note:</em> This is optional data that a transporter may return if the underlying
+     * transport protocol provides metadata (e.g. HTTP headers) along with the actual resource data.
+     * 
+     * @return The (read-only) checksums advertised for the downloaded resource, possibly empty but never {@code null}.
+     */
+    public Map<String, String> getChecksums()
+    {
+        return checksums;
+    }
+
+    /**
+     * Sets a checksum which the remote repository advertises for the resource. <em>Note:</em> Transporters should only
+     * use this method to record checksum information which is readily available while performing the actual download,
+     * they should not perform additional transfers to gather this data.
+     * 
+     * @param algorithm The name of the checksum algorithm (e.g. {@code "SHA-1"}, cf.
+     *            {@link java.security.MessageDigest#getInstance(String)} ), may be {@code null}.
+     * @param value The hexadecimal representation of the checksum, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    public GetTask setChecksum( String algorithm, String value )
+    {
+        if ( algorithm != null )
+        {
+            if ( checksums.isEmpty() )
+            {
+                checksums = new HashMap<String, String>();
+            }
+            if ( value != null && value.length() > 0 )
+            {
+                checksums.put( algorithm, value );
+            }
+            else
+            {
+                checksums.remove( algorithm );
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "<< " + getLocation();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PeekTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PeekTask.java
new file mode 100644
index 0000000..d1fb905
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PeekTask.java
@@ -0,0 +1,50 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+
+/**
+ * A task to check the existence of a resource in the remote repository. <em>Note:</em> The listener returned from
+ * {@link #getListener()} is always a noop given that none of its event methods are relevant in context of this task.
+ * 
+ * @see Transporter#peek(PeekTask)
+ */
+public final class PeekTask
+    extends TransportTask
+{
+
+    /**
+     * Creates a new task for the specified remote resource.
+     * 
+     * @param location The relative location of the resource in the remote repository, must not be {@code null}.
+     */
+    public PeekTask( URI location )
+    {
+        setLocation( location );
+    }
+
+    @Override
+    public String toString()
+    {
+        return "?? " + getLocation();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java
new file mode 100644
index 0000000..1c30e07
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java
@@ -0,0 +1,150 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A task to upload a resource to the remote repository.
+ *
+ * @see Transporter#put(PutTask)
+ */
+public final class PutTask
+    extends TransportTask
+{
+
+    private File dataFile;
+
+    private byte[] dataBytes = EMPTY;
+
+    /**
+     * Creates a new task for the specified remote resource.
+     * 
+     * @param location The relative location of the resource in the remote repository, must not be {@code null}.
+     */
+    public PutTask( URI location )
+    {
+        setLocation( location );
+    }
+
+    /**
+     * Opens an input stream for the data to be uploaded. The length of the stream can be queried via
+     * {@link #getDataLength()}. It's the responsibility of the caller to close the provided stream.
+     * 
+     * @return The input stream for the data, never {@code null}. The stream is unbuffered.
+     * @throws IOException If the stream could not be opened.
+     */
+    public InputStream newInputStream()
+        throws IOException
+    {
+        if ( dataFile != null )
+        {
+            return new FileInputStream( dataFile );
+        }
+        return new ByteArrayInputStream( dataBytes );
+    }
+
+    /**
+     * Gets the total number of bytes to be uploaded.
+     * 
+     * @return The total number of bytes to be uploaded.
+     */
+    public long getDataLength()
+    {
+        if ( dataFile != null )
+        {
+            return dataFile.length();
+        }
+        return dataBytes.length;
+    }
+
+    /**
+     * Gets the file (if any) with the data to be uploaded.
+     * 
+     * @return The data file or {@code null} if the data resides in memory.
+     */
+    public File getDataFile()
+    {
+        return dataFile;
+    }
+
+    /**
+     * Sets the file with the data to be uploaded. To upload some data residing already in memory, use
+     * {@link #setDataString(String)} or {@link #setDataBytes(byte[])}.
+     * 
+     * @param dataFile The data file, may be {@code null} if the resource data is provided directly from memory.
+     * @return This task for chaining, never {@code null}.
+     */
+    public PutTask setDataFile( File dataFile )
+    {
+        this.dataFile = dataFile;
+        dataBytes = EMPTY;
+        return this;
+    }
+
+    /**
+     * Sets the binary data to be uploaded.
+     * 
+     * @param bytes The binary data, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    public PutTask setDataBytes( byte[] bytes )
+    {
+        this.dataBytes = ( bytes != null ) ? bytes : EMPTY;
+        dataFile = null;
+        return this;
+    }
+
+    /**
+     * Sets the textual data to be uploaded. The text is encoded using UTF-8 before transmission.
+     *
+     * @param str The textual data, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    public PutTask setDataString( String str )
+    {
+        return setDataBytes( ( str != null ) ? str.getBytes( StandardCharsets.UTF_8 ) : null );
+    }
+
+    /**
+     * Sets the listener that is to be notified during the transfer.
+     *
+     * @param listener The listener to notify of progress, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    public PutTask setListener( TransportListener listener )
+    {
+        super.setListener( listener );
+        return this;
+    }
+
+    @Override
+    public String toString()
+    {
+        return ">> " + getLocation();
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportListener.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportListener.java
new file mode 100644
index 0000000..473036b
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportListener.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+/**
+ * A skeleton class for listeners used to monitor transport operations. Reusing common regular expression syntax, the
+ * sequence of events is generally as follows:
+ * 
+ * <pre>
+ * ( STARTED PROGRESSED* )*
+ * </pre>
+ * 
+ * The methods in this class do nothing.
+ */
+public abstract class TransportListener
+{
+
+    /**
+     * Enables subclassing.
+     */
+    protected TransportListener()
+    {
+    }
+
+    /**
+     * Notifies the listener about the start of the data transfer. This event may arise more than once if the transfer
+     * needs to be restarted (e.g. after an authentication failure).
+     * 
+     * @param dataOffset The byte offset in the resource at which the transfer starts, must not be negative.
+     * @param dataLength The total number of bytes in the resource or {@code -1} if the length is unknown.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+    }
+
+    /**
+     * Notifies the listener about some progress in the data transfer. This event may even be fired if actually zero
+     * bytes have been transferred since the last event, for instance to enable cancellation.
+     * 
+     * @param data The (read-only) buffer holding the bytes that have just been tranferred, must not be {@code null}.
+     * @throws TransferCancelledException If the transfer should be aborted.
+     */
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportTask.java
new file mode 100644
index 0000000..05525b1
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransportTask.java
@@ -0,0 +1,86 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A transport task.
+ *
+ * @noextend This class is not intended to be extended by clients.
+ */
+public abstract class TransportTask
+{
+
+    static final TransportListener NOOP = new TransportListener()
+    {
+    };
+
+    static final byte[] EMPTY = {};
+
+    private URI location;
+
+    private TransportListener listener = NOOP;
+
+    TransportTask()
+    {
+        // hide
+    }
+
+    /**
+     * Gets the relative location of the affected resource in the remote repository.
+     * 
+     * @return The relative location of the resource, never {@code null}.
+     */
+    public URI getLocation()
+    {
+        return location;
+    }
+
+    TransportTask setLocation( URI location )
+    {
+        this.location = requireNonNull( location, "location type cannot be null" );
+        return this;
+    }
+
+    /**
+     * Gets the listener that is to be notified during the transfer.
+     *
+     * @return The listener to notify of progress, never {@code null}.
+     */
+    public TransportListener getListener()
+    {
+        return listener;
+    }
+
+    /**
+     * Sets the listener that is to be notified during the transfer.
+     * 
+     * @param listener The listener to notify of progress, may be {@code null}.
+     * @return This task for chaining, never {@code null}.
+     */
+    TransportTask setListener( TransportListener listener )
+    {
+        this.listener = ( listener != null ) ? listener : NOOP;
+        return this;
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/Transporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/Transporter.java
new file mode 100644
index 0000000..b8d221c
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/Transporter.java
@@ -0,0 +1,104 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+
+/**
+ * A transporter for a remote repository. A transporter is responsible for transferring resources between the remote
+ * repository and the local system. During its operation, the transporter must provide progress feedback via the
+ * {@link TransportListener} configured on the underlying task.
+ * <p>
+ * If applicable, a transporter should obey connect/request timeouts and other relevant settings from the
+ * {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties() configuration properties} of the repository
+ * system session.
+ * <p>
+ * <strong>Note:</strong> Implementations must be thread-safe such that a given transporter instance can safely be used
+ * for concurrent requests.
+ */
+public interface Transporter
+    extends Closeable
+{
+
+    /**
+     * Classification for exceptions that denote connectivity or authentication issues and any other kind of error that
+     * is not mapped to another classification code.
+     * 
+     * @see #classify(Throwable)
+     */
+    int ERROR_OTHER = 0;
+
+    /**
+     * Classification for exceptions that denote a requested resource does not exist in the remote repository. Note that
+     * cases where a remote repository is completely inaccessible should be classified as {@link #ERROR_OTHER}.
+     * 
+     * @see #classify(Throwable)
+     */
+    int ERROR_NOT_FOUND = 1;
+
+    /**
+     * Classifies the type of exception that has been thrown from a previous request to the transporter. The exception
+     * types employed by a transporter are generally unknown to its caller. Where a caller needs to distinguish between
+     * certain error cases, it employs this method to detect which error case corresponds to the exception.
+     * 
+     * @param error The exception to classify, must not be {@code null}.
+     * @return The classification of the error, either {@link #ERROR_NOT_FOUND} or {@link #ERROR_OTHER}.
+     */
+    int classify( Throwable error );
+
+    /**
+     * Checks the existence of a resource in the repository. If the remote repository can be contacted successfully but
+     * indicates the resource specified in the request does not exist, an exception is thrown such that invoking
+     * {@link #classify(Throwable)} with that exception yields {@link #ERROR_NOT_FOUND}.
+     * 
+     * @param task The existence check to perform, must not be {@code null}.
+     * @throws Exception If the existence of the specified resource could not be confirmed.
+     */
+    void peek( PeekTask task )
+        throws Exception;
+
+    /**
+     * Downloads a resource from the repository. If the resource is downloaded to a file as given by
+     * {@link GetTask#getDataFile()} and the operation fails midway, the transporter should not delete the partial file
+     * but leave its management to the caller.
+     * 
+     * @param task The download to perform, must not be {@code null}.
+     * @throws Exception If the transfer failed.
+     */
+    void get( GetTask task )
+        throws Exception;
+
+    /**
+     * Uploads a resource to the repository.
+     * 
+     * @param task The upload to perform, must not be {@code null}.
+     * @throws Exception If the transfer failed.
+     */
+    void put( PutTask task )
+        throws Exception;
+
+    /**
+     * Closes this transporter and frees any network resources associated with it. Once closed, a transporter must not
+     * be used for further transfers, any attempt to do so would yield a {@link IllegalStateException} or similar.
+     * Closing an already closed transporter is harmless and has no effect.
+     */
+    void close();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterFactory.java
new file mode 100644
index 0000000..999908a
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterFactory.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A factory to create transporters. A transporter is responsible for uploads/downloads to/from a remote repository
+ * using a particular transport protocol. When the repository system needs a transporter for a given remote repository,
+ * it iterates the registered factories in descending order of their priority and calls
+ * {@link #newInstance(RepositorySystemSession, RemoteRepository)} on them. The first transporter returned by a factory
+ * will then be used for the transfer.
+ */
+public interface TransporterFactory
+{
+
+    /**
+     * Tries to create a transporter for the specified remote repository. Typically, a factory will inspect
+     * {@link RemoteRepository#getProtocol()} to determine whether it can handle a repository.
+     * 
+     * @param session The repository system session from which to configure the transporter, must not be {@code null}.
+     *            In particular, a transporter should obey the timeouts configured for the session.
+     * @param repository The remote repository to create a transporter for, must not be {@code null}.
+     * @return The transporter for the given repository, never {@code null}.
+     * @throws NoTransporterException If the factory cannot create a transporter for the specified remote repository.
+     */
+    Transporter newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException;
+
+    /**
+     * The priority of this factory. When multiple factories can handle a given repository, factories with higher
+     * priority are preferred over those with lower priority.
+     * 
+     * @return The priority of this factory.
+     */
+    float getPriority();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterProvider.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterProvider.java
new file mode 100644
index 0000000..b855042
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/TransporterProvider.java
@@ -0,0 +1,47 @@
+package org.eclipse.aether.spi.connector.transport;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * Retrieves a transporter from the installed transporter factories.
+ * 
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface TransporterProvider
+{
+
+    /**
+     * Tries to create a transporter for the specified remote repository.
+     * 
+     * @param session The repository system session from which to configure the transporter, must not be {@code null}.
+     * @param repository The remote repository to create a transporter for, must not be {@code null}.
+     * @return The transporter for the given repository, never {@code null}.
+     * @throws NoTransporterException If none of the installed transporter factories can provide a transporter for the
+     *             specified remote repository.
+     */
+    Transporter newTransporter( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException;
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/package-info.java
new file mode 100644
index 0000000..26796ba
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/package-info.java
@@ -0,0 +1,26 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The contract to download/upload URI-based resources using custom transport protocols. By implementing a
+ * {@link org.eclipse.aether.spi.connector.transport.TransporterFactory} and registering it with the repository system,
+ * an application enables access to remote repositories that use new URI schemes.  
+ */
+package org.eclipse.aether.spi.connector.transport;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
new file mode 100644
index 0000000..1de21a0
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.spi.io;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * A utility component to perform file-based operations.
+ */
+public interface FileProcessor
+{
+
+    /**
+     * Creates the directory named by the given abstract pathname, including any necessary but nonexistent parent
+     * directories. Note that if this operation fails it may have succeeded in creating some of the necessary parent
+     * directories.
+     * 
+     * @param directory The directory to create, may be {@code null}.
+     * @return {@code true} if and only if the directory was created, along with all necessary parent directories;
+     *         {@code false} otherwise
+     */
+    boolean mkdirs( File directory );
+
+    /**
+     * Writes the given data to a file. UTF-8 is assumed as encoding for the data. Creates the necessary directories for
+     * the target file. In case of an error, the created directories will be left on the file system.
+     * 
+     * @param target The file to write to, must not be {@code null}. This file will be overwritten.
+     * @param data The data to write, may be {@code null}.
+     * @throws IOException If an I/O error occurs.
+     */
+    void write( File target, String data )
+        throws IOException;
+
+    /**
+     * Writes the given stream to a file. Creates the necessary directories for the target file. In case of an error,
+     * the created directories will be left on the file system.
+     * 
+     * @param target The file to write to, must not be {@code null}. This file will be overwritten.
+     * @param source The stream to write to the file, must not be {@code null}.
+     * @throws IOException If an I/O error occurs.
+     */
+    void write( File target, InputStream source )
+        throws IOException;
+
+    /**
+     * Moves the specified source file to the given target file. If the target file already exists, it is overwritten.
+     * Creates the necessary directories for the target file. In case of an error, the created directories will be left
+     * on the file system.
+     * 
+     * @param source The file to move from, must not be {@code null}.
+     * @param target The file to move to, must not be {@code null}.
+     * @throws IOException If an I/O error occurs.
+     */
+    void move( File source, File target )
+        throws IOException;
+
+    /**
+     * Copies the specified source file to the given target file. Creates the necessary directories for the target file.
+     * In case of an error, the created directories will be left on the file system.
+     * 
+     * @param source The file to copy from, must not be {@code null}.
+     * @param target The file to copy to, must not be {@code null}.
+     * @throws IOException If an I/O error occurs.
+     */
+    void copy( File source, File target )
+        throws IOException;
+
+    /**
+     * Copies the specified source file to the given target file. Creates the necessary directories for the target file.
+     * In case of an error, the created directories will be left on the file system.
+     * 
+     * @param source The file to copy from, must not be {@code null}.
+     * @param target The file to copy to, must not be {@code null}.
+     * @param listener The listener to notify about the copy progress, may be {@code null}.
+     * @return The number of copied bytes.
+     * @throws IOException If an I/O error occurs.
+     */
+    long copy( File source, File target, ProgressListener listener )
+        throws IOException;
+
+    /**
+     * A listener object that is notified for every progress made while copying files.
+     * 
+     * @see FileProcessor#copy(File, File, ProgressListener)
+     */
+    public interface ProgressListener
+    {
+
+        void progressed( ByteBuffer buffer )
+            throws IOException;
+
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/package-info.java
new file mode 100644
index 0000000..ec5c122
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * I/O related support infrastructure for components. 
+ */
+package org.eclipse.aether.spi.io;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/LocalRepositoryManagerFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/LocalRepositoryManagerFactory.java
new file mode 100644
index 0000000..518f90e
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/LocalRepositoryManagerFactory.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.spi.localrepo;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+
+/**
+ * A factory to create managers for the local repository. A local repository manager needs to keep track of artifacts
+ * and metadata and manage access. When the repository system needs a repository manager for a given local repository,
+ * it iterates the registered factories in descending order of their priority and calls
+ * {@link #newInstance(RepositorySystemSession, LocalRepository)} on them. The first manager returned by a factory will
+ * then be used for the local repository.
+ */
+public interface LocalRepositoryManagerFactory
+{
+
+    /**
+     * Tries to create a repository manager for the specified local repository. The distinguishing property of a local
+     * repository is its {@link LocalRepository#getContentType() type}, which may for example denote the used directory
+     * structure.
+     * 
+     * @param session The repository system session from which to configure the manager, must not be {@code null}.
+     * @param repository The local repository to create a manager for, must not be {@code null}.
+     * @return The manager for the given repository, never {@code null}.
+     * @throws NoLocalRepositoryManagerException If the factory cannot create a manager for the specified local
+     *             repository.
+     */
+    LocalRepositoryManager newInstance( RepositorySystemSession session, LocalRepository repository )
+        throws NoLocalRepositoryManagerException;
+
+    /**
+     * The priority of this factory. Factories with higher priority are preferred over those with lower priority.
+     * 
+     * @return The priority of this factory.
+     */
+    float getPriority();
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/package-info.java
new file mode 100644
index 0000000..afd64cf
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/localrepo/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The contract for custom local repository implementations. 
+ */
+package org.eclipse.aether.spi.localrepo;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/Service.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/Service.java
new file mode 100644
index 0000000..ffe36b0
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/Service.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.spi.locator;
+
+/*
+ * 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.
+ */
+
+/**
+ * A stateless component of the repository system. The primary purpose of this interface is to provide a convenient
+ * means to programmatically wire the several components of the repository system together when it is used outside of an
+ * IoC container.
+ */
+public interface Service
+{
+
+    /**
+     * Provides the opportunity to initialize this service and to acquire other services for its operation from the
+     * locator. A service must not save the reference to the provided service locator.
+     * 
+     * @param locator The service locator, must not be {@code null}.
+     */
+    void initService( ServiceLocator locator );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/ServiceLocator.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/ServiceLocator.java
new file mode 100644
index 0000000..0160ac9
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/ServiceLocator.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.spi.locator;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+/**
+ * A simple infrastructure to programmatically wire the various components of the repository system together when it is
+ * used outside of an IoC container. Once a concrete implementation of a service locator has been setup, clients could
+ * use
+ * 
+ * <pre>
+ * RepositorySystem repoSystem = serviceLocator.getService( RepositorySystem.class );
+ * </pre>
+ * 
+ * to acquire the repository system. Components that implement {@link Service} will be given an opportunity to acquire
+ * further components from the locator, thereby allowing to create the complete object graph of the repository system.
+ */
+public interface ServiceLocator
+{
+
+    /**
+     * Gets an instance of the specified service.
+     * 
+     * @param <T> The service type.
+     * @param type The interface describing the service, must not be {@code null}.
+     * @return The service instance or {@code null} if the service could not be located/initialized.
+     */
+    <T> T getService( Class<T> type );
+
+    /**
+     * Gets all available instances of the specified service.
+     * 
+     * @param <T> The service type.
+     * @param type The interface describing the service, must not be {@code null}.
+     * @return The (read-only) list of available service instances, never {@code null}.
+     */
+    <T> List<T> getServices( Class<T> type );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/package-info.java
new file mode 100644
index 0000000..2d47ceb
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/locator/package-info.java
@@ -0,0 +1,31 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * A lightweight service locator infrastructure to help components acquire dependent components. The implementation of
+ * the repository system is decomposed into many sub components that interact with each other via interfaces, allowing
+ * an application to customize the system by swapping in different implementation classes for these interfaces. The
+ * service locator defined by this package is one means for components to get hold of the proper implementation for its
+ * dependencies. While not the most popular approach to component wiring, this service locator enables applications
+ * that do not wish to pull in more sophisticated solutions like dependency injection containers to have a small
+ * footprint. Therefore, all components should implement {@link org.eclipse.aether.spi.locator.Service} to support this
+ * goal. 
+ */
+package org.eclipse.aether.spi.locator;
+
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/Logger.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/Logger.java
new file mode 100644
index 0000000..8b4bfb3
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/Logger.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.spi.log;
+
+/*
+ * 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.
+ */
+
+/**
+ * A simple logger to facilitate emission of diagnostic messages. In general, unrecoverable errors should be reported
+ * via exceptions and informational notifications should be reported via events, hence this logger interface focuses on
+ * support for tracing.
+ */
+public interface Logger
+{
+
+    /**
+     * Indicates whether debug logging is enabled.
+     * 
+     * @return {@code true} if debug logging is enabled, {@code false} otherwise.
+     */
+    boolean isDebugEnabled();
+
+    /**
+     * Emits the specified message.
+     * 
+     * @param msg The message to log, must not be {@code null}.
+     */
+    void debug( String msg );
+
+    /**
+     * Emits the specified message along with a stack trace of the given exception.
+     * 
+     * @param msg The message to log, must not be {@code null}.
+     * @param error The exception to log, may be {@code null}.
+     */
+    void debug( String msg, Throwable error );
+
+    /**
+     * Indicates whether warn logging is enabled.
+     * 
+     * @return {@code true} if warn logging is enabled, {@code false} otherwise.
+     */
+    boolean isWarnEnabled();
+
+    /**
+     * Emits the specified message.
+     * 
+     * @param msg The message to log, must not be {@code null}.
+     */
+    void warn( String msg );
+
+    /**
+     * Emits the specified message along with a stack trace of the given exception.
+     * 
+     * @param msg The message to log, must not be {@code null}.
+     * @param error The exception to log, may be {@code null}.
+     */
+    void warn( String msg, Throwable error );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/LoggerFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/LoggerFactory.java
new file mode 100644
index 0000000..9f66eb1
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/LoggerFactory.java
@@ -0,0 +1,36 @@
+package org.eclipse.aether.spi.log;
+
+/*
+ * 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.
+ */
+
+/**
+ * A factory to create loggers.
+ */
+public interface LoggerFactory
+{
+
+    /**
+     * Gets a logger for a class with the specified name.
+     * 
+     * @param name The name of the class requesting a logger, must not be {@code null}.
+     * @return The requested logger, never {@code null}.
+     */
+    Logger getLogger( String name );
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLogger.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLogger.java
new file mode 100644
index 0000000..8fb7745
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLogger.java
@@ -0,0 +1,55 @@
+package org.eclipse.aether.spi.log;
+
+/*
+ * 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.
+ */
+
+/**
+ * A logger that disables any logging.
+ */
+final class NullLogger
+    implements Logger
+{
+
+    public boolean isDebugEnabled()
+    {
+        return false;
+    }
+
+    public void debug( String msg )
+    {
+    }
+
+    public void debug( String msg, Throwable error )
+    {
+    }
+
+    public boolean isWarnEnabled()
+    {
+        return false;
+    }
+
+    public void warn( String msg )
+    {
+    }
+
+    public void warn( String msg, Throwable error )
+    {
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLoggerFactory.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLoggerFactory.java
new file mode 100644
index 0000000..bea659f
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/NullLoggerFactory.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.spi.log;
+
+/*
+ * 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.
+ */
+
+/**
+ * A logger factory that disables any logging.
+ */
+public final class NullLoggerFactory
+    implements LoggerFactory
+{
+
+    /**
+     * The singleton instance of this factory.
+     */
+    public static final LoggerFactory INSTANCE = new NullLoggerFactory();
+
+    /**
+     * The singleton logger used by this factory.
+     */
+    public static final Logger LOGGER = new NullLogger();
+
+    public Logger getLogger( String name )
+    {
+        return LOGGER;
+    }
+
+    private NullLoggerFactory()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Gets a logger from the specified factory for the given class, falling back to a logger from this factory if the
+     * specified factory is {@code null} or fails to provide a logger.
+     * 
+     * @param loggerFactory The logger factory from which to get the logger, may be {@code null}.
+     * @param type The class for which to get the logger, must not be {@code null}.
+     * @return The requested logger, never {@code null}.
+     */
+    public static Logger getSafeLogger( LoggerFactory loggerFactory, Class<?> type )
+    {
+        if ( loggerFactory == null )
+        {
+            return LOGGER;
+        }
+        Logger logger = loggerFactory.getLogger( type.getName() );
+        if ( logger == null )
+        {
+            return LOGGER;
+        }
+        return logger;
+    }
+
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/package-info.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/package-info.java
new file mode 100644
index 0000000..9584292
--- /dev/null
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/log/package-info.java
@@ -0,0 +1,28 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * A simple logging infrastructure for diagnostic messages. The primary purpose of the
+ * {@link org.eclipse.aether.spi.log.LoggerFactory} defined here is to avoid a mandatory dependency on a 3rd party
+ * logging system/facade. Some applications might find the events fired by the repository system sufficient and prefer
+ * a small footprint. Components that do not share this concern are free to ignore this package and directly employ
+ * whatever logging system they desire. 
+ */
+package org.eclipse.aether.spi.log;
+
diff --git a/maven-resolver-spi/src/site/site.xml b/maven-resolver-spi/src/site/site.xml
new file mode 100644
index 0000000..27a4aac
--- /dev/null
+++ b/maven-resolver-spi/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="SPI">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/layout/ChecksumTest.java b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/layout/ChecksumTest.java
new file mode 100644
index 0000000..bcd49b4
--- /dev/null
+++ b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/layout/ChecksumTest.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.spi.connector.layout;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.net.URI;
+
+import org.junit.Test;
+
+import org.eclipse.aether.spi.connector.layout.RepositoryLayout.Checksum;
+
+public class ChecksumTest
+{
+
+    @Test
+    public void testForLocation()
+    {
+        Checksum cs = Checksum.forLocation( URI.create( "dir/sub%20dir/file.txt" ), "SHA-1" );
+        assertEquals( "SHA-1", cs.getAlgorithm() );
+        assertEquals( "dir/sub%20dir/file.txt.sha1", cs.getLocation().toString() );
+
+        cs = Checksum.forLocation( URI.create( "dir/sub%20dir/file.txt" ), "MD5" );
+        assertEquals( "MD5", cs.getAlgorithm() );
+        assertEquals( "dir/sub%20dir/file.txt.md5", cs.getLocation().toString() );
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void testForLocation_WithQueryParams()
+    {
+        Checksum.forLocation( URI.create( "file.php?param=1" ), "SHA-1" );
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void testForLocation_WithFragment()
+    {
+        Checksum.forLocation( URI.create( "file.html#fragment" ), "SHA-1" );
+    }
+
+}
diff --git a/maven-resolver-test-util/pom.xml b/maven-resolver-test-util/pom.xml
new file mode 100644
index 0000000..9bc2f27
--- /dev/null
+++ b/maven-resolver-test-util/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-test-util</artifactId>
+
+  <name>Maven Artifact Resolver Test Utilities</name>
+  <description>
+    A collection of utility classes to ease testing of the repository system.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.testutil</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDefinition.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDefinition.java
new file mode 100644
index 0000000..0a760cc
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDefinition.java
@@ -0,0 +1,140 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+class ArtifactDefinition
+{
+    private String groupId;
+
+    private String artifactId;
+
+    private String extension;
+
+    private String version;
+
+    private String scope = "";
+
+    private String definition;
+
+    private String id;
+
+    private String reference;
+
+    private Boolean optional;
+
+    public ArtifactDefinition( String def )
+    {
+        this.definition = def.trim();
+
+        if ( definition.startsWith( "(" ) )
+        {
+            int idx = definition.indexOf( ')' );
+            this.id = definition.substring( 1, idx );
+            this.definition = definition.substring( idx + 1 );
+        }
+        else if ( definition.startsWith( "^" ) )
+        {
+            this.reference = definition.substring( 1 );
+            return;
+        }
+
+        String[] split = definition.split( ":" );
+        if ( split.length < 4 )
+        {
+            throw new IllegalArgumentException( "Need definition like 'gid:aid:ext:ver[:scope]', but was: "
+                + definition );
+        }
+        groupId = split[0];
+        artifactId = split[1];
+        extension = split[2];
+        version = split[3];
+        if ( split.length > 4 )
+        {
+            scope = split[4];
+        }
+        if ( split.length > 5 )
+        {
+            if ( "optional".equalsIgnoreCase( split[5] ) )
+            {
+                optional = true;
+            }
+            else if ( "!optional".equalsIgnoreCase( split[5] ) )
+            {
+                optional = false;
+            }
+        }
+    }
+
+    public String getGroupId()
+    {
+        return groupId;
+    }
+
+    public String getArtifactId()
+    {
+        return artifactId;
+    }
+
+    public String getExtension()
+    {
+        return extension;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public String getScope()
+    {
+        return scope;
+    }
+
+    @Override
+    public String toString()
+    {
+        return definition;
+    }
+
+    public String getId()
+    {
+        return id;
+    }
+
+    public String getReference()
+    {
+        return reference;
+    }
+
+    public boolean isReference()
+    {
+        return reference != null;
+    }
+
+    public boolean hasId()
+    {
+        return id != null;
+    }
+
+    public Boolean getOptional()
+    {
+        return optional;
+    }
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDescription.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDescription.java
new file mode 100644
index 0000000..bdb5c7f
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/ArtifactDescription.java
@@ -0,0 +1,70 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ */
+class ArtifactDescription
+{
+
+    private List<RemoteRepository> repositories;
+
+    private List<Dependency> managedDependencies;
+
+    private List<Dependency> dependencies;
+
+    private Artifact relocation;
+
+    ArtifactDescription( Artifact relocation, List<Dependency> dependencies, List<Dependency> managedDependencies,
+                         List<RemoteRepository> repositories )
+    {
+        this.relocation = relocation;
+        this.dependencies = dependencies;
+        this.managedDependencies = managedDependencies;
+        this.repositories = repositories;
+    }
+
+    public Artifact getRelocation()
+    {
+        return relocation;
+    }
+
+    public List<RemoteRepository> getRepositories()
+    {
+        return repositories;
+    }
+
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    public List<Dependency> getDependencies()
+    {
+        return dependencies;
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java
new file mode 100644
index 0000000..3bdeaa6
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java
@@ -0,0 +1,564 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines
+ * one node of the resulting graph:
+ * 
+ * <pre>
+ * line      ::= (indent? ("(null)" | node | reference))? comment?
+ * comment   ::= "#" rest-of-line
+ * indent    ::= "|  "*  ("+" | "\\") "- "
+ * reference ::= "^" id
+ * node      ::= coords (range)? space (scope("&lt;" premanagedScope)?)? space "optional"? space ("relocations=" coords ("," coords)*)? ("(" id ")")?
+ * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
+ * </pre>
+ * 
+ * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
+ * <p>
+ * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
+ * calculated by the distance from the beginning of the line. One level is three characters of indentation.
+ * <p>
+ * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
+ * nodes.
+ * <h2>Example</h2>
+ * 
+ * <pre>
+ * gid:aid:ver
+ * +- gid:aid2:ver scope
+ * |  \- gid:aid3:ver        (id1)    # assign id for reference below
+ * +- gid:aid4:ext:ver scope
+ * \- ^id1                            # reuse previous node
+ * </pre>
+ * 
+ * <h2>Multiple definitions in one resource</h2>
+ * <p>
+ * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
+ * same resource. The rest of the line is ignored.
+ * <h2>Substitutions</h2>
+ * <p>
+ * You may define substitutions (see {@link #setSubstitutions(String...)},
+ * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
+ * String in the defined substitutions.
+ * <h3>Example</h3>
+ * 
+ * <pre>
+ * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
+ * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
+ * </pre>
+ * 
+ * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
+ * artifact id.
+ */
+public class DependencyGraphParser
+{
+
+    private final VersionScheme versionScheme;
+
+    private final String prefix;
+
+    private Collection<String> substitutions;
+
+    /**
+     * Create a parser with the given prefix and the given substitution strings.
+     * 
+     * @see DependencyGraphParser#parseResource(String)
+     */
+    public DependencyGraphParser( String prefix, Collection<String> substitutions )
+    {
+        this.prefix = prefix;
+        this.substitutions = substitutions;
+        versionScheme = new TestVersionScheme();
+    }
+
+    /**
+     * Create a parser with the given prefix.
+     * 
+     * @see DependencyGraphParser#parseResource(String)
+     */
+    public DependencyGraphParser( String prefix )
+    {
+        this( prefix, Collections.<String>emptyList() );
+    }
+
+    /**
+     * Create a parser with an empty prefix.
+     */
+    public DependencyGraphParser()
+    {
+        this( "" );
+    }
+
+    /**
+     * Parse the given graph definition.
+     */
+    public DependencyNode parseLiteral( String dependencyGraph )
+        throws IOException
+    {
+        BufferedReader reader = new BufferedReader( new StringReader( dependencyGraph ) );
+        DependencyNode node = parse( reader );
+        reader.close();
+        return node;
+    }
+
+    /**
+     * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the
+     * resource from 'prefix + resource'.
+     */
+    public DependencyNode parseResource( String resource )
+        throws IOException
+    {
+        URL res = this.getClass().getClassLoader().getResource( prefix + resource );
+        if ( res == null )
+        {
+            throw new IOException( "Could not find classpath resource " + prefix + resource );
+        }
+        return parse( res );
+    }
+
+    /**
+     * Parse multiple graphs in one resource, divided by "---".
+     */
+    public List<DependencyNode> parseMultiResource( String resource )
+        throws IOException
+    {
+        URL res = this.getClass().getClassLoader().getResource( prefix + resource );
+        if ( res == null )
+        {
+            throw new IOException( "Could not find classpath resource " + prefix + resource );
+        }
+
+        BufferedReader reader = new BufferedReader( new InputStreamReader( res.openStream(), StandardCharsets.UTF_8 ) );
+
+        List<DependencyNode> ret = new ArrayList<DependencyNode>();
+        DependencyNode root = null;
+        while ( ( root = parse( reader ) ) != null )
+        {
+            ret.add( root );
+        }
+        return ret;
+    }
+
+    /**
+     * Parse the graph definition read from the given URL.
+     */
+    public DependencyNode parse( URL resource )
+        throws IOException
+    {
+        BufferedReader reader = null;
+        try
+        {
+            reader = new BufferedReader( new InputStreamReader( resource.openStream(), StandardCharsets.UTF_8 ) );
+            final DependencyNode node = parse( reader );
+            return node;
+        }
+        finally
+        {
+            try
+            {
+                if ( reader != null )
+                {
+                    reader.close();
+                    reader = null;
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    private DependencyNode parse( BufferedReader in )
+        throws IOException
+    {
+        Iterator<String> substitutionIterator = ( substitutions != null ) ? substitutions.iterator() : null;
+
+        String line = null;
+
+        DependencyNode root = null;
+        DependencyNode node = null;
+        int prevLevel = 0;
+
+        Map<String, DependencyNode> nodes = new HashMap<String, DependencyNode>();
+        LinkedList<DependencyNode> stack = new LinkedList<DependencyNode>();
+        boolean isRootNode = true;
+
+        while ( ( line = in.readLine() ) != null )
+        {
+            line = cutComment( line );
+
+            if ( isEmpty( line ) )
+            {
+                // skip empty line
+                continue;
+            }
+
+            if ( isEOFMarker( line ) )
+            {
+                // stop parsing
+                break;
+            }
+
+            while ( line.contains( "%s" ) )
+            {
+                if ( !substitutionIterator.hasNext() )
+                {
+                    throw new IllegalStateException( "not enough substitutions to fill placeholders" );
+                }
+                line = line.replaceFirst( "%s", substitutionIterator.next() );
+            }
+
+            LineContext ctx = createContext( line );
+            if ( prevLevel < ctx.getLevel() )
+            {
+                // previous node is new parent
+                stack.add( node );
+            }
+
+            // get to real parent
+            while ( prevLevel > ctx.getLevel() )
+            {
+                stack.removeLast();
+                prevLevel -= 1;
+            }
+
+            prevLevel = ctx.getLevel();
+
+            if ( ctx.getDefinition() != null && ctx.getDefinition().reference != null )
+            {
+                String reference = ctx.getDefinition().reference;
+                DependencyNode child = nodes.get( reference );
+                if ( child == null )
+                {
+                    throw new IllegalStateException( "undefined reference " + reference );
+                }
+                node.getChildren().add( child );
+            }
+            else
+            {
+
+                node = build( isRootNode ? null : stack.getLast(), ctx, isRootNode );
+
+                if ( isRootNode )
+                {
+                    root = node;
+                    isRootNode = false;
+                }
+
+                if ( ctx.getDefinition() != null && ctx.getDefinition().id != null )
+                {
+                    nodes.put( ctx.getDefinition().id, node );
+                }
+            }
+        }
+
+        return root;
+    }
+
+    private boolean isEOFMarker( String line )
+    {
+        return line.startsWith( "---" );
+    }
+
+    private static boolean isEmpty( String line )
+    {
+        return line == null || line.length() == 0;
+    }
+
+    private static String cutComment( String line )
+    {
+        int idx = line.indexOf( '#' );
+
+        if ( idx != -1 )
+        {
+            line = line.substring( 0, idx );
+        }
+
+        return line;
+    }
+
+    private DependencyNode build( DependencyNode parent, LineContext ctx, boolean isRoot )
+    {
+        NodeDefinition def = ctx.getDefinition();
+        if ( !isRoot && parent == null )
+        {
+            throw new IllegalStateException( "dangling node: " + def );
+        }
+        else if ( ctx.getLevel() == 0 && parent != null )
+        {
+            throw new IllegalStateException( "inconsistent leveling (parent for level 0?): " + def );
+        }
+
+        DefaultDependencyNode node;
+        if ( def != null )
+        {
+            DefaultArtifact artifact = new DefaultArtifact( def.coords, def.properties );
+            Dependency dependency = new Dependency( artifact, def.scope, def.optional );
+            node = new DefaultDependencyNode( dependency );
+            int managedBits = 0;
+            if ( def.premanagedScope != null )
+            {
+                managedBits |= DependencyNode.MANAGED_SCOPE;
+                node.setData( "premanaged.scope", def.premanagedScope );
+            }
+            if ( def.premanagedVersion != null )
+            {
+                managedBits |= DependencyNode.MANAGED_VERSION;
+                node.setData( "premanaged.version", def.premanagedVersion );
+            }
+            node.setManagedBits( managedBits );
+            if ( def.relocations != null )
+            {
+                List<Artifact> relocations = new ArrayList<Artifact>();
+                for ( String relocation : def.relocations )
+                {
+                    relocations.add( new DefaultArtifact( relocation ) );
+                }
+                node.setRelocations( relocations );
+            }
+            try
+            {
+                node.setVersion( versionScheme.parseVersion( artifact.getVersion() ) );
+                node.setVersionConstraint( versionScheme.parseVersionConstraint( def.range != null ? def.range
+                                : artifact.getVersion() ) );
+            }
+            catch ( InvalidVersionSpecificationException e )
+            {
+                throw new IllegalArgumentException( "bad version: " + e.getMessage(), e );
+            }
+        }
+        else
+        {
+            node = new DefaultDependencyNode( (Dependency) null );
+        }
+
+        if ( parent != null )
+        {
+            parent.getChildren().add( node );
+        }
+
+        return node;
+    }
+
+    public String dump( DependencyNode root )
+    {
+        StringBuilder ret = new StringBuilder();
+
+        List<NodeEntry> entries = new ArrayList<NodeEntry>();
+
+        addNode( root, 0, entries );
+
+        for ( NodeEntry nodeEntry : entries )
+        {
+            char[] level = new char[( nodeEntry.getLevel() * 3 )];
+            Arrays.fill( level, ' ' );
+
+            if ( level.length != 0 )
+            {
+                level[level.length - 3] = '+';
+                level[level.length - 2] = '-';
+            }
+
+            String definition = nodeEntry.getDefinition();
+
+            ret.append( level ).append( definition ).append( "\n" );
+        }
+
+        return ret.toString();
+
+    }
+
+    private void addNode( DependencyNode root, int level, List<NodeEntry> entries )
+    {
+
+        NodeEntry entry = new NodeEntry();
+        Dependency dependency = root.getDependency();
+        StringBuilder defBuilder = new StringBuilder();
+        if ( dependency == null )
+        {
+            defBuilder.append( "(null)" );
+        }
+        else
+        {
+            Artifact artifact = dependency.getArtifact();
+
+            defBuilder.append( artifact.getGroupId() ).append( ":" ).append( artifact.getArtifactId() ).append( ":" ).append( artifact.getExtension() ).append( ":" ).append( artifact.getVersion() );
+            if ( dependency.getScope() != null && ( !"".equals( dependency.getScope() ) ) )
+            {
+                defBuilder.append( ":" ).append( dependency.getScope() );
+            }
+
+            Map<String, String> properties = artifact.getProperties();
+            if ( !( properties == null || properties.isEmpty() ) )
+            {
+                for ( Map.Entry<String, String> prop : properties.entrySet() )
+                {
+                    defBuilder.append( ";" ).append( prop.getKey() ).append( "=" ).append( prop.getValue() );
+                }
+            }
+        }
+
+        entry.setDefinition( defBuilder.toString() );
+        entry.setLevel( level++ );
+
+        entries.add( entry );
+
+        for ( DependencyNode node : root.getChildren() )
+        {
+            addNode( node, level, entries );
+        }
+
+    }
+
+    class NodeEntry
+    {
+        int level;
+
+        String definition;
+
+        Map<String, String> properties;
+
+        public int getLevel()
+        {
+            return level;
+        }
+
+        public void setLevel( int level )
+        {
+            this.level = level;
+        }
+
+        public String getDefinition()
+        {
+            return definition;
+        }
+
+        public void setDefinition( String definition )
+        {
+            this.definition = definition;
+        }
+
+        public Map<String, String> getProperties()
+        {
+            return properties;
+        }
+
+        public void setProperties( Map<String, String> properties )
+        {
+            this.properties = properties;
+        }
+    }
+
+    private static LineContext createContext( String line )
+    {
+        LineContext ctx = new LineContext();
+        String definition;
+
+        String[] split = line.split( "- " );
+        if ( split.length == 1 ) // root
+        {
+            ctx.setLevel( 0 );
+            definition = split[0];
+        }
+        else
+        {
+            ctx.setLevel( (int) Math.ceil( (double) split[0].length() / (double) 3 ) );
+            definition = split[1];
+        }
+
+        if ( "(null)".equalsIgnoreCase( definition ) )
+        {
+            return ctx;
+        }
+
+        ctx.setDefinition( new NodeDefinition( definition ) );
+
+        return ctx;
+    }
+
+    static class LineContext
+    {
+        NodeDefinition definition;
+
+        int level;
+
+        public NodeDefinition getDefinition()
+        {
+            return definition;
+        }
+
+        public void setDefinition( NodeDefinition definition )
+        {
+            this.definition = definition;
+        }
+
+        public int getLevel()
+        {
+            return level;
+        }
+
+        public void setLevel( int level )
+        {
+            this.level = level;
+        }
+    }
+
+    public Collection<String> getSubstitutions()
+    {
+        return substitutions;
+    }
+
+    public void setSubstitutions( Collection<String> substitutions )
+    {
+        this.substitutions = substitutions;
+    }
+
+    public void setSubstitutions( String... substitutions )
+    {
+        setSubstitutions( Arrays.asList( substitutions ) );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDataReader.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDataReader.java
new file mode 100644
index 0000000..063a135
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDataReader.java
@@ -0,0 +1,397 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * @see IniArtifactDescriptorReader
+ */
+class IniArtifactDataReader
+{
+
+    private String prefix = "";
+
+    /**
+     * Constructs a data reader with the prefix {@code ""}.
+     */
+    public IniArtifactDataReader()
+    {
+        this( "" );
+    }
+
+    /**
+     * Constructs a data reader with the given prefix.
+     * 
+     * @param prefix the prefix to use for loading resources from the classpath.
+     */
+    public IniArtifactDataReader( String prefix )
+    {
+        this.prefix = prefix;
+
+    }
+
+    /**
+     * Load an artifact description from the classpath and parse it.
+     */
+    public ArtifactDescription parse( String resource )
+        throws IOException
+    {
+        URL res = this.getClass().getClassLoader().getResource( prefix + resource );
+
+        if ( res == null )
+        {
+            throw new IOException( "cannot find resource: " + resource );
+        }
+        return parse( res );
+    }
+
+    /**
+     * Open the given URL and parse ist.
+     */
+    public ArtifactDescription parse( URL res )
+        throws IOException
+    {
+        return parse( new InputStreamReader( res.openStream(), StandardCharsets.UTF_8 ) );
+    }
+
+    /**
+     * Parse the given String.
+     */
+    public ArtifactDescription parseLiteral( String description )
+        throws IOException
+    {
+        StringReader reader = new StringReader( description );
+        return parse( reader );
+    }
+
+    private enum State
+    {
+        NONE, RELOCATION, DEPENDENCIES, MANAGEDDEPENDENCIES, REPOSITORIES
+    }
+
+    private ArtifactDescription parse( Reader reader )
+        throws IOException
+    {
+        String line = null;
+
+        State state = State.NONE;
+
+        Map<State, List<String>> sections = new HashMap<State, List<String>>();
+
+        BufferedReader in = null;
+        try
+        {
+            in = new BufferedReader( reader );
+            while ( ( line = in.readLine() ) != null )
+            {
+
+                line = cutComment( line );
+                if ( isEmpty( line ) )
+                {
+                    continue;
+                }
+
+                if ( line.startsWith( "[" ) )
+                {
+                    try
+                    {
+                        String name = line.substring( 1, line.length() - 1 );
+                        name = name.replace( "-", "" ).toUpperCase( Locale.ENGLISH );
+                        state = State.valueOf( name );
+                        sections.put( state, new ArrayList<String>() );
+                    }
+                    catch ( IllegalArgumentException e )
+                    {
+                        throw new IOException( "unknown section: " + line );
+                    }
+                }
+                else
+                {
+                    List<String> lines = sections.get( state );
+                    if ( lines == null )
+                    {
+                        throw new IOException( "missing section: " + line );
+                    }
+                    lines.add( line.trim() );
+                }
+            }
+
+            in.close();
+            in = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( in != null )
+                {
+                    in.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+
+        Artifact relocation = relocation( sections.get( State.RELOCATION ) );
+        List<Dependency> dependencies = dependencies( sections.get( State.DEPENDENCIES ), false );
+        List<Dependency> managedDependencies = dependencies( sections.get( State.MANAGEDDEPENDENCIES ), true );
+        List<RemoteRepository> repositories = repositories( sections.get( State.REPOSITORIES ) );
+
+        ArtifactDescription description =
+            new ArtifactDescription( relocation, dependencies, managedDependencies, repositories );
+        return description;
+    }
+
+    private List<RemoteRepository> repositories( List<String> list )
+    {
+        ArrayList<RemoteRepository> ret = new ArrayList<RemoteRepository>();
+        if ( list == null )
+        {
+            return ret;
+        }
+        for ( String coords : list )
+        {
+            String[] split = coords.split( ":", 3 );
+            String id = split[0];
+            String type = split[1];
+            String url = split[2];
+
+            ret.add( new RemoteRepository.Builder( id, type, url ).build() );
+        }
+        return ret;
+    }
+
+    private List<Dependency> dependencies( List<String> list, boolean managed )
+    {
+        List<Dependency> ret = new ArrayList<Dependency>();
+        if ( list == null )
+        {
+            return ret;
+        }
+
+        Collection<Exclusion> exclusions = new ArrayList<Exclusion>();
+
+        Boolean optional = null;
+        Artifact artifact = null;
+        String scope = null;
+
+        for ( String coords : list )
+        {
+            if ( coords.startsWith( "-" ) )
+            {
+                coords = coords.substring( 1 );
+                String[] split = coords.split( ":" );
+                exclusions.add( new Exclusion( split[0], split[1], "*", "*" ) );
+            }
+            else
+            {
+                if ( artifact != null )
+                {
+                    // commit dependency
+                    Dependency dep = new Dependency( artifact, scope, optional, exclusions );
+                    ret.add( dep );
+
+                    exclusions = new ArrayList<Exclusion>();
+                }
+
+                ArtifactDefinition def = new ArtifactDefinition( coords );
+
+                optional = managed ? def.getOptional() : Boolean.valueOf( Boolean.TRUE.equals( def.getOptional() ) );
+
+                scope = "".equals( def.getScope() ) && !managed ? "compile" : def.getScope();
+
+                artifact =
+                    new DefaultArtifact( def.getGroupId(), def.getArtifactId(), "", def.getExtension(),
+                                         def.getVersion() );
+            }
+        }
+        if ( artifact != null )
+        {
+            // commit dependency
+            Dependency dep = new Dependency( artifact, scope, optional, exclusions );
+            ret.add( dep );
+        }
+
+        return ret;
+    }
+
+    private Artifact relocation( List<String> list )
+    {
+        if ( list == null || list.isEmpty() )
+        {
+            return null;
+        }
+        String coords = list.get( 0 );
+        ArtifactDefinition def = new ArtifactDefinition( coords );
+        return new DefaultArtifact( def.getGroupId(), def.getArtifactId(), "", def.getExtension(), def.getVersion() );
+    }
+
+    private static boolean isEmpty( String line )
+    {
+        return line == null || line.length() == 0;
+    }
+
+    private static String cutComment( String line )
+    {
+        int idx = line.indexOf( '#' );
+
+        if ( idx != -1 )
+        {
+            line = line.substring( 0, idx );
+        }
+
+        return line;
+    }
+
+    static class Definition
+    {
+        private String groupId;
+
+        private String artifactId;
+
+        private String extension;
+
+        private String version;
+
+        private String scope = "";
+
+        private String definition;
+
+        private String id = null;
+
+        private String reference = null;
+
+        private boolean optional = false;
+
+        public Definition( String def )
+        {
+            this.definition = def.trim();
+
+            if ( definition.startsWith( "(" ) )
+            {
+                int idx = definition.indexOf( ')' );
+                this.id = definition.substring( 1, idx );
+                this.definition = definition.substring( idx + 1 );
+            }
+            else if ( definition.startsWith( "^" ) )
+            {
+                this.reference = definition.substring( 1 );
+                return;
+            }
+
+            String[] split = definition.split( ":" );
+            if ( split.length < 4 )
+            {
+                throw new IllegalArgumentException( "Need definition like 'gid:aid:ext:ver[:scope]', but was: "
+                    + definition );
+            }
+            groupId = split[0];
+            artifactId = split[1];
+            extension = split[2];
+            version = split[3];
+            if ( split.length > 4 )
+            {
+                scope = split[4];
+            }
+            if ( split.length > 5 && "true".equalsIgnoreCase( split[5] ) )
+            {
+                optional = true;
+            }
+        }
+
+        public String getGroupId()
+        {
+            return groupId;
+        }
+
+        public String getArtifactId()
+        {
+            return artifactId;
+        }
+
+        public String getType()
+        {
+            return extension;
+        }
+
+        public String getVersion()
+        {
+            return version;
+        }
+
+        public String getScope()
+        {
+            return scope;
+        }
+
+        @Override
+        public String toString()
+        {
+            return definition;
+        }
+
+        public String getId()
+        {
+            return id;
+        }
+
+        public String getReference()
+        {
+            return reference;
+        }
+
+        public boolean isReference()
+        {
+            return reference != null;
+        }
+
+        public boolean hasId()
+        {
+            return id != null;
+        }
+
+        public boolean isOptional()
+        {
+            return optional;
+        }
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReader.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReader.java
new file mode 100644
index 0000000..4efe880
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReader.java
@@ -0,0 +1,125 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+
+/**
+ * An artifact descriptor reader that gets data from a simple text file on the classpath. The data file for an artifact
+ * with the coordinates {@code gid:aid:ext:ver} is expected to be named {@code gid_aid_ver.ini} and can optionally have
+ * some prefix. The data file can have the following sections:
+ * <ul>
+ * <li>relocation</li>
+ * <li>dependencies</li>
+ * <li>managedDependencies</li>
+ * <li>repositories</li>
+ * </ul>
+ * The relocation and dependency sections contain artifact coordinates of the form:
+ * 
+ * <pre>
+ * gid:aid:ext:ver[:scope][:optional]
+ * </pre>
+ * 
+ * The dependency sections may also specify exclusions:
+ * 
+ * <pre>
+ * -gid:aid
+ * </pre>
+ * 
+ * A repository definition is of the form:
+ * 
+ * <pre>
+ * id:type:url
+ * </pre>
+ * 
+ * <h2>Example</h2>
+ * 
+ * <pre>
+ * [relocation]
+ * gid:aid:ext:ver
+ * 
+ * [dependencies]
+ * gid:aid:ext:ver:scope
+ * -exclusion:aid
+ * gid:aid2:ext:ver:scope:optional
+ * 
+ * [managed-dependencies]
+ * gid:aid2:ext:ver2:scope
+ * -gid:aid
+ * -gid:aid
+ * 
+ * [repositories]
+ * id:type:file:///test-repo
+ * </pre>
+ */
+public class IniArtifactDescriptorReader
+{
+    private IniArtifactDataReader reader;
+
+    /**
+     * Use the given prefix to load the artifact descriptions from the classpath.
+     */
+    public IniArtifactDescriptorReader( String prefix )
+    {
+        reader = new IniArtifactDataReader( prefix );
+    }
+
+    /**
+     * Parses the resource {@code $prefix/gid_aid_ver.ini} from the request artifact as an artifact description and
+     * wraps it into an ArtifactDescriptorResult.
+     */
+    public ArtifactDescriptorResult readArtifactDescriptor( RepositorySystemSession session,
+                                                            ArtifactDescriptorRequest request )
+        throws ArtifactDescriptorException
+    {
+        ArtifactDescriptorResult result = new ArtifactDescriptorResult( request );
+        for ( Artifact artifact = request.getArtifact();; )
+        {
+            String resourceName =
+                String.format( "%s_%s_%s.ini", artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion() );
+            try
+            {
+                ArtifactDescription data = reader.parse( resourceName );
+                if ( data.getRelocation() != null )
+                {
+                    result.addRelocation( artifact );
+                    artifact = data.getRelocation();
+                }
+                else
+                {
+                    result.setArtifact( artifact );
+                    result.setDependencies( data.getDependencies() );
+                    result.setManagedDependencies( data.getManagedDependencies() );
+                    result.setRepositories( data.getRepositories() );
+                    return result;
+                }
+            }
+            catch ( Exception e )
+            {
+                throw new ArtifactDescriptorException( result, e.getMessage(), e );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeBuilder.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeBuilder.java
new file mode 100644
index 0000000..fdd988a
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeBuilder.java
@@ -0,0 +1,164 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * A builder to create dependency nodes for unit testing.
+ */
+public class NodeBuilder
+{
+
+    private String groupId = "test";
+
+    private String artifactId = "";
+
+    private String version = "0.1";
+
+    private String range;
+
+    private String ext = "jar";
+
+    private String classifier = "";
+
+    private String scope = "compile";
+
+    private boolean optional = false;
+
+    private String context;
+
+    private List<Artifact> relocations = new ArrayList<Artifact>();
+
+    private VersionScheme versionScheme = new TestVersionScheme();
+
+    private Map<String, String> properties = new HashMap<String, String>( 0 );
+
+    public NodeBuilder artifactId( String artifactId )
+    {
+        this.artifactId = artifactId;
+        return this;
+    }
+
+    public NodeBuilder groupId( String groupId )
+    {
+        this.groupId = groupId;
+        return this;
+
+    }
+
+    public NodeBuilder ext( String ext )
+    {
+        this.ext = ext;
+        return this;
+    }
+
+    public NodeBuilder version( String version )
+    {
+        this.version = version;
+        this.range = null;
+        return this;
+    }
+
+    public NodeBuilder range( String range )
+    {
+        this.range = range;
+        return this;
+    }
+
+    public NodeBuilder scope( String scope )
+    {
+        this.scope = scope;
+        return this;
+    }
+
+    public NodeBuilder optional( boolean optional )
+    {
+        this.optional = optional;
+        return this;
+    }
+
+    public NodeBuilder context( String context )
+    {
+        this.context = context;
+        return this;
+    }
+
+    public NodeBuilder reloc( String artifactId )
+    {
+        Artifact relocation = new DefaultArtifact( groupId, artifactId, classifier, ext, version );
+        relocations.add( relocation );
+        return this;
+    }
+
+    public NodeBuilder reloc( String groupId, String artifactId, String version )
+    {
+        Artifact relocation = new DefaultArtifact( groupId, artifactId, classifier, ext, version );
+        relocations.add( relocation );
+        return this;
+    }
+
+    public NodeBuilder properties( Map<String, String> properties )
+    {
+        this.properties = properties != null ? properties : Collections.<String, String>emptyMap();
+        return this;
+    }
+
+    public DependencyNode build()
+    {
+        Dependency dependency = null;
+        if ( artifactId != null && artifactId.length() > 0 )
+        {
+            Artifact artifact =
+                new DefaultArtifact( groupId, artifactId, classifier, ext, version, properties, (File) null );
+            dependency = new Dependency( artifact, scope, optional );
+        }
+        DefaultDependencyNode node = new DefaultDependencyNode( dependency );
+        if ( artifactId != null && artifactId.length() > 0 )
+        {
+            try
+            {
+                node.setVersion( versionScheme.parseVersion( version ) );
+                node.setVersionConstraint( versionScheme.parseVersionConstraint( range != null ? range : version ) );
+            }
+            catch ( InvalidVersionSpecificationException e )
+            {
+                throw new IllegalArgumentException( "bad version: " + e.getMessage(), e );
+            }
+        }
+        node.setRequestContext( context );
+        node.setRelocations( relocations );
+        return node;
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeDefinition.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeDefinition.java
new file mode 100644
index 0000000..64910f1
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/NodeDefinition.java
@@ -0,0 +1,144 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A definition of a dependency node via a single line of text.
+ * 
+ * @see DependencyGraphParser
+ */
+class NodeDefinition
+{
+
+    static final String ID = "\\(([-_a-zA-Z0-9]+)\\)";
+
+    static final String IDREF = "\\^([-_a-zA-Z0-9]+)";
+
+    static final String COORDS = "([^: \\(]+):([^: ]+)(?::([^: ]*)(?::([^: ]+))?)?:([^: \\[\\(<]+)";
+
+    private static final String COORDS_NC = NodeDefinition.COORDS.replaceAll( "\\((?=\\[)", "(?:" );
+
+    private static final String RANGE_NC = "[\\(\\[][^\\(\\)\\[\\]]+[\\)\\]]";
+
+    static final String RANGE = "(" + RANGE_NC + ")";
+
+    static final String SCOPE = "(?:scope\\s*=\\s*)?((?!optional)[-_a-zA-Z0-9]+)(?:<([-_a-zA-Z0-9]+))?";
+
+    static final String OPTIONAL = "(!?optional)";
+
+    static final String RELOCATIONS = "relocations\\s*=\\s*(" + COORDS_NC + "(?:\\s*,\\s*" + COORDS_NC + ")*)";
+
+    static final String KEY_VAL = "(?:[-_a-zA-Z0-9]+)\\s*:\\s*(?:[-_a-zA-Z0-9]*)";
+
+    static final String PROPS = "props\\s*=\\s*(" + KEY_VAL + "(?:\\s*,\\s*" + KEY_VAL + ")*)";
+
+    static final String COORDSX = "(" + COORDS_NC + ")" + RANGE + "?(?:<((?:" + RANGE_NC + ")|\\S+))?";
+
+    static final String NODE = COORDSX + "(?:\\s+" + PROPS + ")?" + "(?:\\s+" + SCOPE + ")?" + "(?:\\s+" + OPTIONAL
+        + ")?" + "(?:\\s+" + RELOCATIONS + ")?" + "(?:\\s+" + ID + ")?";
+
+    static final String LINE = "(?:" + IDREF + ")|(?:" + NODE + ")";
+
+    private static final Pattern PATTERN = Pattern.compile( LINE );
+
+    private final String def;
+
+    String coords;
+
+    Map<String, String> properties;
+
+    String range;
+
+    String premanagedVersion;
+
+    String scope;
+
+    String premanagedScope;
+
+    Boolean optional;
+
+    List<String> relocations;
+
+    String id;
+
+    String reference;
+
+    public NodeDefinition( String definition )
+    {
+        def = definition.trim();
+
+        Matcher m = PATTERN.matcher( def );
+        if ( !m.matches() )
+        {
+            throw new IllegalArgumentException( "bad syntax: " + def );
+        }
+
+        reference = m.group( 1 );
+        if ( reference != null )
+        {
+            return;
+        }
+
+        coords = m.group( 2 );
+        range = m.group( 3 );
+        premanagedVersion = m.group( 4 );
+
+        String props = m.group( 5 );
+        if ( props != null )
+        {
+            properties = new LinkedHashMap<String, String>();
+            for ( String prop : props.split( "\\s*,\\s*" ) )
+            {
+                int sep = prop.indexOf( ':' );
+                String key = prop.substring( 0, sep );
+                String val = prop.substring( sep + 1 );
+                properties.put( key, val );
+            }
+        }
+
+        scope = m.group( 6 );
+        premanagedScope = m.group( 7 );
+        optional = ( m.group( 8 ) != null ) ? !m.group( 8 ).startsWith( "!" ) : Boolean.FALSE;
+
+        String relocs = m.group( 9 );
+        if ( relocs != null )
+        {
+            relocations = new ArrayList<String>();
+            Collections.addAll( relocations, relocs.split( "\\s*,\\s*" ) );
+        }
+
+        id = m.group( 10 );
+    }
+
+    @Override
+    public String toString()
+    {
+        return def;
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyCollectionContext.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyCollectionContext.java
new file mode 100644
index 0000000..b4f4155
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyCollectionContext.java
@@ -0,0 +1,78 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ */
+final class TestDependencyCollectionContext
+    implements DependencyCollectionContext
+{
+
+    private final RepositorySystemSession session;
+
+    private final Artifact artifact;
+
+    private final Dependency dependency;
+
+    private final List<Dependency> managedDependencies;
+
+    public TestDependencyCollectionContext( RepositorySystemSession session, Artifact artifact, Dependency dependency,
+                                            List<Dependency> managedDependencies )
+    {
+        this.session = session;
+        this.artifact = ( dependency != null ) ? dependency.getArtifact() : artifact;
+        this.dependency = dependency;
+        this.managedDependencies = managedDependencies;
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Artifact getArtifact()
+    {
+        return artifact;
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public List<Dependency> getManagedDependencies()
+    {
+        return managedDependencies;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getDependency() );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyGraphTransformationContext.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyGraphTransformationContext.java
new file mode 100644
index 0000000..d1fcdbc
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestDependencyGraphTransformationContext.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+
+/**
+ */
+class TestDependencyGraphTransformationContext
+    implements DependencyGraphTransformationContext
+{
+
+    private final RepositorySystemSession session;
+
+    private final Map<Object, Object> map;
+
+    public TestDependencyGraphTransformationContext( RepositorySystemSession session )
+    {
+        this.session = session;
+        this.map = new HashMap<Object, Object>();
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Object get( Object key )
+    {
+        return map.get( requireNonNull( key, "key cannot be null" ) );
+    }
+
+    public Object put( Object key, Object value )
+    {
+        requireNonNull( key, "key cannot be null" );
+        if ( value != null )
+        {
+            return map.put( key, value );
+        }
+        else
+        {
+            return map.remove( key );
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( map );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileProcessor.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileProcessor.java
new file mode 100644
index 0000000..118ef13
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileProcessor.java
@@ -0,0 +1,250 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.aether.spi.io.FileProcessor;
+
+/**
+ * A simple file processor implementation to help satisfy component requirements during tests.
+ */
+public class TestFileProcessor
+    implements FileProcessor
+{
+
+    public boolean mkdirs( File directory )
+    {
+        if ( directory == null )
+        {
+            return false;
+        }
+
+        if ( directory.exists() )
+        {
+            return false;
+        }
+        if ( directory.mkdir() )
+        {
+            return true;
+        }
+
+        File canonDir = null;
+        try
+        {
+            canonDir = directory.getCanonicalFile();
+        }
+        catch ( IOException e )
+        {
+            return false;
+        }
+
+        File parentDir = canonDir.getParentFile();
+        return ( parentDir != null && ( mkdirs( parentDir ) || parentDir.exists() ) && canonDir.mkdir() );
+    }
+
+    public void write( File file, String data )
+        throws IOException
+    {
+        mkdirs( file.getParentFile() );
+
+        FileOutputStream fos = null;
+        try
+        {
+            fos = new FileOutputStream( file );
+
+            if ( data != null )
+            {
+                fos.write( data.getBytes( StandardCharsets.UTF_8 ) );
+            }
+
+            fos.close();
+            fos = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fos != null )
+                {
+                    fos.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public void write( File target, InputStream source )
+        throws IOException
+    {
+        mkdirs( target.getAbsoluteFile().getParentFile() );
+
+        OutputStream fos = null;
+        try
+        {
+            fos = new BufferedOutputStream( new FileOutputStream( target ) );
+
+            copy( fos, source, null );
+
+            fos.close();
+            fos = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fos != null )
+                {
+                    fos.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public void copy( File source, File target )
+        throws IOException
+    {
+        copy( source, target, null );
+    }
+
+    public long copy( File source, File target, ProgressListener listener )
+        throws IOException
+    {
+        long total = 0;
+
+        InputStream fis = null;
+        OutputStream fos = null;
+        try
+        {
+            fis = new FileInputStream( source );
+
+            mkdirs( target.getAbsoluteFile().getParentFile() );
+
+            fos = new BufferedOutputStream( new FileOutputStream( target ) );
+
+            total = copy( fos, fis, listener );
+
+            fos.close();
+            fos = null;
+
+            fis.close();
+            fis = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fos != null )
+                {
+                    fos.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+            finally
+            {
+                try
+                {
+                    if ( fis != null )
+                    {
+                        fis.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+        }
+
+        return total;
+    }
+
+    private long copy( OutputStream os, InputStream is, ProgressListener listener )
+        throws IOException
+    {
+        long total = 0L;
+
+        ByteBuffer buffer = ByteBuffer.allocate( 1024 * 32 );
+        byte[] array = buffer.array();
+
+        while ( true )
+        {
+            int bytes = is.read( array );
+            if ( bytes < 0 )
+            {
+                break;
+            }
+
+            os.write( array, 0, bytes );
+
+            total += bytes;
+
+            if ( listener != null && bytes > 0 )
+            {
+                try
+                {
+                    buffer.rewind();
+                    buffer.limit( bytes );
+                    listener.progressed( buffer );
+                }
+                catch ( Exception e )
+                {
+                    // too bad
+                }
+            }
+        }
+
+        return total;
+    }
+
+    public void move( File source, File target )
+        throws IOException
+    {
+        target.delete();
+
+        if ( !source.renameTo( target ) )
+        {
+            copy( source, target );
+
+            target.setLastModified( source.lastModified() );
+
+            source.delete();
+        }
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileUtils.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileUtils.java
new file mode 100644
index 0000000..f59199f
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestFileUtils.java
@@ -0,0 +1,378 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Properties;
+import java.util.UUID;
+
+/**
+ * Provides utility methods to read and write (temporary) files.
+ */
+public class TestFileUtils
+{
+
+    private static final File TMP = new File( System.getProperty( "java.io.tmpdir" ), "aether-"
+        + UUID.randomUUID().toString().substring( 0, 8 ) );
+
+    static
+    {
+        Runtime.getRuntime().addShutdownHook( new Thread()
+        {
+            @Override
+            public void run()
+            {
+                try
+                {
+                    deleteFile( TMP );
+                }
+                catch ( IOException e )
+                {
+                    e.printStackTrace();
+                }
+            }
+        } );
+    }
+
+    private TestFileUtils()
+    {
+        // hide constructor
+    }
+
+    public static void deleteTempFiles()
+        throws IOException
+    {
+        deleteFile( TMP );
+    }
+
+    public static void deleteFile( File file )
+        throws IOException
+    {
+        if ( file == null )
+        {
+            return;
+        }
+
+        Collection<File> undeletables = new ArrayList<File>();
+
+        delete( file, undeletables );
+
+        if ( !undeletables.isEmpty() )
+        {
+            throw new IOException( "Failed to delete " + undeletables );
+        }
+    }
+
+    private static void delete( File file, Collection<File> undeletables )
+    {
+        String[] children = file.list();
+        if ( children != null )
+        {
+            for ( String child : children )
+            {
+                delete( new File( file, child ), undeletables );
+            }
+        }
+
+        if ( !del( file ) )
+        {
+            undeletables.add( file.getAbsoluteFile() );
+        }
+    }
+
+    private static boolean del( File file )
+    {
+        for ( int i = 0; i < 10; i++ )
+        {
+            if ( file.delete() || !file.exists() )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean mkdirs( File directory )
+    {
+        if ( directory == null )
+        {
+            return false;
+        }
+
+        if ( directory.exists() )
+        {
+            return false;
+        }
+        if ( directory.mkdir() )
+        {
+            return true;
+        }
+
+        File canonDir = null;
+        try
+        {
+            canonDir = directory.getCanonicalFile();
+        }
+        catch ( IOException e )
+        {
+            return false;
+        }
+
+        File parentDir = canonDir.getParentFile();
+        return ( parentDir != null && ( mkdirs( parentDir ) || parentDir.exists() ) && canonDir.mkdir() );
+    }
+
+    public static File createTempFile( String contents )
+        throws IOException
+    {
+        return createTempFile( contents.getBytes( StandardCharsets.UTF_8 ), 1 );
+    }
+
+    public static File createTempFile( byte[] pattern, int repeat )
+        throws IOException
+    {
+        mkdirs( TMP );
+        File tmpFile = File.createTempFile( "tmpfile-", ".data", TMP );
+        writeBytes( tmpFile, pattern, repeat );
+        return tmpFile;
+    }
+
+    public static File createTempDir()
+        throws IOException
+    {
+        return createTempDir( "" );
+    }
+
+    public static File createTempDir( String suffix )
+        throws IOException
+    {
+        mkdirs( TMP );
+        File tmpFile = File.createTempFile( "tmpdir-", suffix, TMP );
+        deleteFile( tmpFile );
+        mkdirs( tmpFile );
+        return tmpFile;
+    }
+
+    public static long copyFile( File source, File target )
+        throws IOException
+    {
+        long total = 0;
+
+        FileInputStream fis = null;
+        OutputStream fos = null;
+        try
+        {
+            fis = new FileInputStream( source );
+
+            mkdirs( target.getParentFile() );
+
+            fos = new BufferedOutputStream( new FileOutputStream( target ) );
+
+            for ( byte[] buffer = new byte[ 1024 * 32 ];; )
+            {
+                int bytes = fis.read( buffer );
+                if ( bytes < 0 )
+                {
+                    break;
+                }
+
+                fos.write( buffer, 0, bytes );
+
+                total += bytes;
+            }
+
+            fos.close();
+            fos = null;
+
+            fis.close();
+            fis = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fos != null )
+                {
+                    fos.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+            finally
+            {
+                try
+                {
+                    if ( fis != null )
+                    {
+                        fis.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+            }
+        }
+
+        return total;
+    }
+
+    public static byte[] readBytes( File file )
+        throws IOException
+    {
+        RandomAccessFile in = null;
+        try
+        {
+            in = new RandomAccessFile( file, "r" );
+            byte[] actual = new byte[ (int) in.length() ];
+            in.readFully( actual );
+            in.close();
+            in = null;
+            return actual;
+        }
+        finally
+        {
+            try
+            {
+                if ( in != null )
+                {
+                    in.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public static void writeBytes( File file, byte[] pattern, int repeat )
+        throws IOException
+    {
+        file.deleteOnExit();
+        file.getParentFile().mkdirs();
+        OutputStream out = null;
+        try
+        {
+            out = new BufferedOutputStream( new FileOutputStream( file ) );
+            for ( int i = 0; i < repeat; i++ )
+            {
+                out.write( pattern );
+            }
+            out.close();
+            out = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( out != null )
+                {
+                    out.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public static String readString( File file )
+        throws IOException
+    {
+        byte[] content = readBytes( file );
+        return new String( content, StandardCharsets.UTF_8 );
+    }
+
+    public static void writeString( File file, String content )
+        throws IOException
+    {
+        writeBytes( file, content.getBytes( StandardCharsets.UTF_8 ), 1 );
+    }
+
+    public static void readProps( File file, Properties props )
+        throws IOException
+    {
+        FileInputStream fis = null;
+        try
+        {
+            fis = new FileInputStream( file );
+            props.load( fis );
+            fis.close();
+            fis = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fis != null )
+                {
+                    fis.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+    public static void writeProps( File file, Properties props )
+        throws IOException
+    {
+        file.getParentFile().mkdirs();
+
+        FileOutputStream fos = null;
+        try
+        {
+            fos = new FileOutputStream( file );
+            props.store( fos, "aether-test" );
+            fos.close();
+            fos = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( fos != null )
+                {
+                    fos.close();
+                }
+            }
+            catch ( final IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLocalRepositoryManager.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLocalRepositoryManager.java
new file mode 100644
index 0000000..f97fb78
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLocalRepositoryManager.java
@@ -0,0 +1,159 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A simplistic local repository manager that uses a temporary base directory.
+ */
+public class TestLocalRepositoryManager
+    implements LocalRepositoryManager
+{
+
+    private LocalRepository localRepository;
+
+    private Set<Artifact> unavailableArtifacts = new HashSet<Artifact>();
+
+    private Set<Artifact> artifactRegistrations = new HashSet<Artifact>();
+
+    private Set<Metadata> metadataRegistrations = new HashSet<Metadata>();
+
+    public TestLocalRepositoryManager()
+    {
+        try
+        {
+            localRepository = new LocalRepository( TestFileUtils.createTempDir( "test-local-repo" ) );
+        }
+        catch ( IOException e )
+        {
+            throw new IllegalStateException( e );
+        }
+    }
+
+    public LocalRepository getRepository()
+    {
+        return localRepository;
+    }
+
+    public String getPathForLocalArtifact( Artifact artifact )
+    {
+        String artifactId = artifact.getArtifactId();
+        String groupId = artifact.getGroupId();
+        String extension = artifact.getExtension();
+        String version = artifact.getVersion();
+        String classifier = artifact.getClassifier();
+
+        String path =
+            String.format( "%s/%s/%s/%s-%s-%s%s.%s", groupId, artifactId, version, groupId, artifactId, version,
+                           classifier, extension );
+        return path;
+    }
+
+    public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+    {
+        return getPathForLocalArtifact( artifact );
+    }
+
+    public String getPathForLocalMetadata( Metadata metadata )
+    {
+        String artifactId = metadata.getArtifactId();
+        String groupId = metadata.getGroupId();
+        String version = metadata.getVersion();
+        return String.format( "%s/%s/%s/%s-%s-%s.xml", groupId, artifactId, version, groupId, artifactId, version );
+    }
+
+    public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+    {
+        return getPathForLocalMetadata( metadata );
+    }
+
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    {
+        Artifact artifact = request.getArtifact();
+
+        LocalArtifactResult result = new LocalArtifactResult( request );
+        File file = new File( localRepository.getBasedir(), getPathForLocalArtifact( artifact ) );
+        result.setFile( file.isFile() ? file : null );
+        result.setAvailable( file.isFile() && !unavailableArtifacts.contains( artifact ) );
+
+        return result;
+    }
+
+    public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+    {
+        artifactRegistrations.add( request.getArtifact() );
+    }
+
+    public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
+    {
+        Metadata metadata = request.getMetadata();
+
+        LocalMetadataResult result = new LocalMetadataResult( request );
+        File file = new File( localRepository.getBasedir(), getPathForLocalMetadata( metadata ) );
+        result.setFile( file.isFile() ? file : null );
+
+        return result;
+    }
+
+    public void add( RepositorySystemSession session, LocalMetadataRegistration request )
+    {
+        metadataRegistrations.add( request.getMetadata() );
+    }
+
+    public Set<Artifact> getArtifactRegistration()
+    {
+        return artifactRegistrations;
+    }
+
+    public Set<Metadata> getMetadataRegistration()
+    {
+        return metadataRegistrations;
+    }
+
+    public void setArtifactAvailability( Artifact artifact, boolean available )
+    {
+        if ( available )
+        {
+            unavailableArtifacts.remove( artifact );
+        }
+        else
+        {
+            unavailableArtifacts.add( artifact );
+        }
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLoggerFactory.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLoggerFactory.java
new file mode 100644
index 0000000..ea92825
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestLoggerFactory.java
@@ -0,0 +1,108 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.io.PrintStream;
+
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+
+/**
+ * A logger factory that writes to some {@link PrintStream}.
+ */
+public final class TestLoggerFactory
+    implements LoggerFactory
+{
+
+    private final Logger logger;
+
+    /**
+     * Creates a new logger factory that writes to {@link System#out}.
+     */
+    public TestLoggerFactory()
+    {
+        this( null );
+    }
+
+    /**
+     * Creates a new logger factory that writes to the specified print stream.
+     */
+    public TestLoggerFactory( PrintStream out )
+    {
+        logger = new TestLogger( out );
+    }
+
+    public Logger getLogger( String name )
+    {
+        return logger;
+    }
+
+    private static final class TestLogger
+        implements Logger
+    {
+
+        private final PrintStream out;
+
+        public TestLogger( PrintStream out )
+        {
+            this.out = ( out != null ) ? out : System.out;
+        }
+
+        public boolean isWarnEnabled()
+        {
+            return true;
+        }
+
+        public void warn( String msg, Throwable error )
+        {
+            out.println( "[WARN] " + msg );
+            if ( error != null )
+            {
+                error.printStackTrace( out );
+            }
+        }
+
+        public void warn( String msg )
+        {
+            warn( msg, null );
+        }
+
+        public boolean isDebugEnabled()
+        {
+            return true;
+        }
+
+        public void debug( String msg, Throwable error )
+        {
+            out.println( "[DEBUG] " + msg );
+            if ( error != null )
+            {
+                error.printStackTrace( out );
+            }
+        }
+
+        public void debug( String msg )
+        {
+            debug( msg, null );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestUtils.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestUtils.java
new file mode 100644
index 0000000..cc0c4cb
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestUtils.java
@@ -0,0 +1,92 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.resolution.VersionRangeResult;
+
+/**
+ * Utility methods to help unit testing.
+ */
+public class TestUtils
+{
+
+    private TestUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Creates a new repository session whose local repository manager is initialized with an instance of
+     * {@link TestLocalRepositoryManager}.
+     */
+    public static DefaultRepositorySystemSession newSession()
+    {
+        DefaultRepositorySystemSession session = new DefaultRepositorySystemSession();
+        session.setLocalRepositoryManager( new TestLocalRepositoryManager() );
+        return session;
+    }
+
+    /**
+     * Creates a new dependency collection context.
+     */
+    public static DependencyCollectionContext newCollectionContext( RepositorySystemSession session,
+                                                                    Dependency dependency,
+                                                                    List<Dependency> managedDependencies )
+    {
+        return new TestDependencyCollectionContext( session, null, dependency, managedDependencies );
+    }
+
+    /**
+     * Creates a new dependency collection context.
+     */
+    public static DependencyCollectionContext newCollectionContext( RepositorySystemSession session, Artifact artifact,
+                                                                    Dependency dependency,
+                                                                    List<Dependency> managedDependencies )
+    {
+        return new TestDependencyCollectionContext( session, artifact, dependency, managedDependencies );
+    }
+
+    /**
+     * Creates a new dependency graph transformation context.
+     */
+    public static DependencyGraphTransformationContext newTransformationContext( RepositorySystemSession session )
+    {
+        return new TestDependencyGraphTransformationContext( session );
+    }
+
+    /**
+     * Creates a new version filter context from the specified session and version range result.
+     */
+    public static VersionFilter.VersionFilterContext newVersionFilterContext( RepositorySystemSession session,
+                                                                              VersionRangeResult rangeResult )
+    {
+        return new TestVersionFilterContext( session, rangeResult );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersion.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersion.java
new file mode 100644
index 0000000..0fc9bab
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersion.java
@@ -0,0 +1,88 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.version.Version;
+
+/**
+ * Version ordering by {@link String#compareToIgnoreCase(String)}.
+ */
+final class TestVersion
+    implements Version
+{
+
+    private String version;
+
+    public TestVersion( String version )
+    {
+        this.version = version == null ? "" : version;
+    }
+
+    public int compareTo( Version o )
+    {
+        return version.compareTo( o.toString() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ( ( version == null ) ? 0 : version.hashCode() );
+        return result;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null )
+        {
+            return false;
+        }
+        if ( getClass() != obj.getClass() )
+        {
+            return false;
+        }
+        TestVersion other = (TestVersion) obj;
+        if ( version == null )
+        {
+            if ( other.version != null )
+            {
+                return false;
+            }
+        }
+        else if ( !version.equals( other.version ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString()
+    {
+        return version;
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionConstraint.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionConstraint.java
new file mode 100644
index 0000000..51228ee
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionConstraint.java
@@ -0,0 +1,125 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+
+/**
+ * A constraint on versions for a dependency.
+ */
+final class TestVersionConstraint
+    implements VersionConstraint
+{
+
+    private final VersionRange range;
+
+    private final Version version;
+
+    /**
+     * Creates a version constraint from the specified version range.
+     *
+     * @param range The version range, must not be {@code null}.
+     */
+    public TestVersionConstraint( VersionRange range )
+    {
+        this.range = requireNonNull( range, "version range cannot be null" );
+        this.version = null;
+    }
+
+    /**
+     * Creates a version constraint from the specified version.
+     *
+     * @param version The version, must not be {@code null}.
+     */
+    public TestVersionConstraint( Version version )
+    {
+        this.version = requireNonNull( version, "version cannot be null" );
+        this.range = null;
+    }
+
+    public VersionRange getRange()
+    {
+        return range;
+    }
+
+    public Version getVersion()
+    {
+        return version;
+    }
+
+    public boolean containsVersion( Version version )
+    {
+        if ( range == null )
+        {
+            return version.equals( this.version );
+        }
+        else
+        {
+            return range.containsVersion( version );
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( ( range == null ) ? version : range );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        TestVersionConstraint that = (TestVersionConstraint) obj;
+
+        return eq( range, that.range ) && eq( version, that.getVersion() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( getRange() );
+        hash = hash * 31 + hash( getVersion() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionFilterContext.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionFilterContext.java
new file mode 100644
index 0000000..2647c56
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionFilterContext.java
@@ -0,0 +1,93 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ */
+class TestVersionFilterContext
+    implements VersionFilter.VersionFilterContext
+{
+
+    private final RepositorySystemSession session;
+
+    private final Dependency dependency;
+
+    private final VersionRangeResult result;
+
+    private final List<Version> versions;
+
+    public TestVersionFilterContext( RepositorySystemSession session, VersionRangeResult result )
+    {
+        this.session = session;
+        this.result = result;
+        dependency = new Dependency( result.getRequest().getArtifact(), "" );
+        versions = new ArrayList<Version>( result.getVersions() );
+    }
+
+    public RepositorySystemSession getSession()
+    {
+        return session;
+    }
+
+    public Dependency getDependency()
+    {
+        return dependency;
+    }
+
+    public int getCount()
+    {
+        return versions.size();
+    }
+
+    public Iterator<Version> iterator()
+    {
+        return versions.iterator();
+    }
+
+    public VersionConstraint getVersionConstraint()
+    {
+        return result.getVersionConstraint();
+    }
+
+    public ArtifactRepository getRepository( Version version )
+    {
+        return result.getRepository( version );
+    }
+
+    public List<RemoteRepository> getRepositories()
+    {
+        return Collections.unmodifiableList( result.getRequest().getRepositories() );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionRange.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionRange.java
new file mode 100644
index 0000000..dd5950e
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionRange.java
@@ -0,0 +1,247 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionRange;
+
+/**
+ * A version range inspired by mathematical range syntax. For example, "[1.0,2.0)", "[1.0,)" or "[1.0]".
+ */
+final class TestVersionRange
+    implements VersionRange
+{
+
+    private final Version lowerBound;
+
+    private final boolean lowerBoundInclusive;
+
+    private final Version upperBound;
+
+    private final boolean upperBoundInclusive;
+
+    /**
+     * Creates a version range from the specified range specification.
+     * 
+     * @param range The range specification to parse, must not be {@code null}.
+     * @throws InvalidVersionSpecificationException If the range could not be parsed.
+     */
+    public TestVersionRange( String range )
+        throws InvalidVersionSpecificationException
+    {
+        String process = range;
+
+        if ( range.startsWith( "[" ) )
+        {
+            lowerBoundInclusive = true;
+        }
+        else if ( range.startsWith( "(" ) )
+        {
+            lowerBoundInclusive = false;
+        }
+        else
+        {
+            throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                + ", a range must start with either [ or (" );
+        }
+
+        if ( range.endsWith( "]" ) )
+        {
+            upperBoundInclusive = true;
+        }
+        else if ( range.endsWith( ")" ) )
+        {
+            upperBoundInclusive = false;
+        }
+        else
+        {
+            throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                + ", a range must end with either [ or (" );
+        }
+
+        process = process.substring( 1, process.length() - 1 );
+
+        int index = process.indexOf( "," );
+
+        if ( index < 0 )
+        {
+            if ( !lowerBoundInclusive || !upperBoundInclusive )
+            {
+                throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                    + ", single version must be surrounded by []" );
+            }
+
+            lowerBound = upperBound = new TestVersion( process.trim() );
+        }
+        else
+        {
+            String parsedLowerBound = process.substring( 0, index ).trim();
+            String parsedUpperBound = process.substring( index + 1 ).trim();
+
+            // more than two bounds, e.g. (1,2,3)
+            if ( parsedUpperBound.contains( "," ) )
+            {
+                throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                    + ", bounds may not contain additional ','" );
+            }
+
+            lowerBound = parsedLowerBound.length() > 0 ? new TestVersion( parsedLowerBound ) : null;
+            upperBound = parsedUpperBound.length() > 0 ? new TestVersion( parsedUpperBound ) : null;
+
+            if ( upperBound != null && lowerBound != null )
+            {
+                if ( upperBound.compareTo( lowerBound ) < 0 )
+                {
+                    throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                        + ", lower bound must not be greater than upper bound" );
+                }
+            }
+        }
+    }
+
+    public Bound getLowerBound()
+    {
+        return new Bound( lowerBound, lowerBoundInclusive );
+    }
+
+    public Bound getUpperBound()
+    {
+        return new Bound( upperBound, upperBoundInclusive );
+    }
+
+    public boolean acceptsSnapshots()
+    {
+        return isSnapshot( lowerBound ) || isSnapshot( upperBound );
+    }
+
+    public boolean containsVersion( Version version )
+    {
+        boolean snapshot = isSnapshot( version );
+
+        if ( lowerBound != null )
+        {
+            int comparison = lowerBound.compareTo( version );
+
+            if ( snapshot && comparison == 0 )
+            {
+                return true;
+            }
+
+            if ( comparison == 0 && !lowerBoundInclusive )
+            {
+                return false;
+            }
+            if ( comparison > 0 )
+            {
+                return false;
+            }
+        }
+
+        if ( upperBound != null )
+        {
+            int comparison = upperBound.compareTo( version );
+
+            if ( snapshot && comparison == 0 )
+            {
+                return true;
+            }
+
+            if ( comparison == 0 && !upperBoundInclusive )
+            {
+                return false;
+            }
+            if ( comparison < 0 )
+            {
+                return false;
+            }
+        }
+
+        if ( lowerBound != null || upperBound != null )
+        {
+            return !snapshot;
+        }
+
+        return true;
+    }
+
+    private boolean isSnapshot( Version version )
+    {
+        return version != null && version.toString().endsWith( "SNAPSHOT" );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        TestVersionRange that = (TestVersionRange) obj;
+
+        return upperBoundInclusive == that.upperBoundInclusive && lowerBoundInclusive == that.lowerBoundInclusive
+            && eq( upperBound, that.upperBound ) && eq( lowerBound, that.lowerBound );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( upperBound );
+        hash = hash * 31 + ( upperBoundInclusive ? 1 : 0 );
+        hash = hash * 31 + hash( lowerBound );
+        hash = hash * 31 + ( lowerBoundInclusive ? 1 : 0 );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 64 );
+        buffer.append( lowerBoundInclusive ? '[' : '(' );
+        if ( lowerBound != null )
+        {
+            buffer.append( lowerBound );
+        }
+        buffer.append( ',' );
+        if ( upperBound != null )
+        {
+            buffer.append( upperBound );
+        }
+        buffer.append( upperBoundInclusive ? ']' : ')' );
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionScheme.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionScheme.java
new file mode 100644
index 0000000..5865f6c
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestVersionScheme.java
@@ -0,0 +1,120 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * A version scheme using a generic version syntax.
+ */
+final class TestVersionScheme
+    implements VersionScheme
+{
+
+    public Version parseVersion( final String version )
+        throws InvalidVersionSpecificationException
+    {
+        return new TestVersion( version );
+    }
+
+    public VersionRange parseVersionRange( final String range )
+        throws InvalidVersionSpecificationException
+    {
+        return new TestVersionRange( range );
+    }
+
+    public VersionConstraint parseVersionConstraint( final String constraint )
+        throws InvalidVersionSpecificationException
+    {
+        Collection<VersionRange> ranges = new ArrayList<VersionRange>();
+
+        String process = constraint;
+
+        while ( process.startsWith( "[" ) || process.startsWith( "(" ) )
+        {
+            int index1 = process.indexOf( ')' );
+            int index2 = process.indexOf( ']' );
+
+            int index = index2;
+            if ( index2 < 0 || ( index1 >= 0 && index1 < index2 ) )
+            {
+                index = index1;
+            }
+
+            if ( index < 0 )
+            {
+                throw new InvalidVersionSpecificationException( constraint, "Unbounded version range " + constraint );
+            }
+
+            VersionRange range = parseVersionRange( process.substring( 0, index + 1 ) );
+            ranges.add( range );
+
+            process = process.substring( index + 1 ).trim();
+
+            if ( process.length() > 0 && process.startsWith( "," ) )
+            {
+                process = process.substring( 1 ).trim();
+            }
+        }
+
+        if ( process.length() > 0 && !ranges.isEmpty() )
+        {
+            throw new InvalidVersionSpecificationException( constraint, "Invalid version range " + constraint
+                + ", expected [ or ( but got " + process );
+        }
+
+        VersionConstraint result;
+        if ( ranges.isEmpty() )
+        {
+            result = new TestVersionConstraint( parseVersion( constraint ) );
+        }
+        else
+        {
+            result = new TestVersionConstraint( ranges.iterator().next() );
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean equals( final Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        return obj != null && getClass().equals( obj.getClass() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/package-info.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/package-info.java
new file mode 100644
index 0000000..4d874da
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/package-info.java
@@ -0,0 +1,26 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Utility classes to ease unit testing. This package supports the needs of Aether's own codebase but implementors of
+ * extensions might find some of these classes useful, too, as long as they understand that these classes do not denote
+ * a stable API and are subject to change without prior notice.
+ */
+package org.eclipse.aether.internal.test.util;
+
diff --git a/maven-resolver-test-util/src/site/site.xml b/maven-resolver-test-util/src/site/site.xml
new file mode 100644
index 0000000..32ad754
--- /dev/null
+++ b/maven-resolver-test-util/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Test Utilities">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/DependencyGraphParserTest.java b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/DependencyGraphParserTest.java
new file mode 100644
index 0000000..ac0a368
--- /dev/null
+++ b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/DependencyGraphParserTest.java
@@ -0,0 +1,299 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class DependencyGraphParserTest
+{
+
+    private DependencyGraphParser parser;
+
+    @Before
+    public void setup()
+    {
+        this.parser = new DependencyGraphParser();
+    }
+
+    @Test
+    public void testOnlyRoot()
+        throws IOException
+    {
+        String def = "gid:aid:jar:1 scope";
+
+        DependencyNode node = parser.parseLiteral( def );
+
+        assertNotNull( node );
+        assertEquals( 0, node.getChildren().size() );
+
+        Dependency dependency = node.getDependency();
+        assertNotNull( dependency );
+        assertEquals( "scope", dependency.getScope() );
+
+        Artifact artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "jar", artifact.getExtension() );
+        assertEquals( "1", artifact.getVersion() );
+    }
+
+    @Test
+    public void testOptionalScope()
+        throws IOException
+    {
+        String def = "gid:aid:jar:1";
+
+        DependencyNode node = parser.parseLiteral( def );
+
+        assertNotNull( node );
+        assertEquals( 0, node.getChildren().size() );
+
+        Dependency dependency = node.getDependency();
+        assertNotNull( dependency );
+        assertEquals( "", dependency.getScope() );
+    }
+
+    @Test
+    public void testWithChildren()
+        throws IOException
+    {
+        String def =
+            "gid1:aid1:ext1:ver1 scope1\n" + "+- gid2:aid2:ext2:ver2 scope2\n" + "\\- gid3:aid3:ext3:ver3 scope3\n";
+
+        DependencyNode node = parser.parseLiteral( def );
+        assertNotNull( node );
+
+        int idx = 1;
+
+        assertNodeProperties( node, idx++ );
+
+        List<DependencyNode> children = node.getChildren();
+        assertEquals( 2, children.size() );
+
+        for ( DependencyNode child : children )
+        {
+            assertNodeProperties( child, idx++ );
+        }
+
+    }
+
+    @Test
+    public void testDeepChildren()
+        throws IOException
+    {
+        String def =
+            "gid1:aid1:ext1:ver1\n" + "+- gid2:aid2:ext2:ver2 scope2\n" + "|  \\- gid3:aid3:ext3:ver3\n"
+                + "\\- gid4:aid4:ext4:ver4 scope4";
+
+        DependencyNode node = parser.parseLiteral( def );
+        assertNodeProperties( node, 1 );
+
+        assertEquals( 2, node.getChildren().size() );
+        assertNodeProperties( node.getChildren().get( 1 ), 4 );
+        DependencyNode lvl1Node = node.getChildren().get( 0 );
+        assertNodeProperties( lvl1Node, 2 );
+
+        assertEquals( 1, lvl1Node.getChildren().size() );
+        assertNodeProperties( lvl1Node.getChildren().get( 0 ), 3 );
+    }
+
+    private void assertNodeProperties( DependencyNode node, int idx )
+    {
+        assertNodeProperties( node, String.valueOf( idx ) );
+    }
+
+    private void assertNodeProperties( DependencyNode node, String suffix )
+    {
+        assertNotNull( node );
+        Dependency dependency = node.getDependency();
+        assertNotNull( dependency );
+        if ( !"".equals( dependency.getScope() ) )
+        {
+            assertEquals( "scope" + suffix, dependency.getScope() );
+        }
+
+        Artifact artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+
+        assertEquals( "gid" + suffix, artifact.getGroupId() );
+        assertEquals( "aid" + suffix, artifact.getArtifactId() );
+        assertEquals( "ext" + suffix, artifact.getExtension() );
+        assertEquals( "ver" + suffix, artifact.getVersion() );
+    }
+
+    @Test
+    public void testComments()
+        throws IOException
+    {
+        String def = "# first line\n#second line\ngid:aid:ext:ver # root artifact asdf:qwer:zcxv:uip";
+
+        DependencyNode node = parser.parseLiteral( def );
+
+        assertNodeProperties( node, "" );
+    }
+
+    @Test
+    public void testId()
+        throws IOException
+    {
+        String def = "gid:aid:ext:ver (id)\n\\- ^id";
+        DependencyNode node = parser.parseLiteral( def );
+        assertNodeProperties( node, "" );
+
+        assertNotNull( node.getChildren() );
+        assertEquals( 1, node.getChildren().size() );
+
+        assertSame( node, node.getChildren().get( 0 ) );
+    }
+
+    @Test
+    public void testResourceLoading()
+        throws IOException
+    {
+        String prefix = "org/eclipse/aether/internal/test/util/";
+        String name = "testResourceLoading.txt";
+
+        DependencyNode node = parser.parseResource( prefix + name );
+        assertEquals( 0, node.getChildren().size() );
+        assertNodeProperties( node, "" );
+    }
+
+    @Test
+    public void testResourceLoadingWithPrefix()
+        throws IOException
+    {
+        String prefix = "org/eclipse/aether/internal/test/util/";
+        parser = new DependencyGraphParser( prefix );
+
+        String name = "testResourceLoading.txt";
+
+        DependencyNode node = parser.parseResource( name );
+        assertEquals( 0, node.getChildren().size() );
+        assertNodeProperties( node, "" );
+    }
+
+    @Test
+    public void testProperties()
+        throws IOException
+    {
+        String def = "gid:aid:ext:ver props=test:foo,test2:fizzle";
+        DependencyNode node = parser.parseLiteral( def );
+
+        assertNodeProperties( node, "" );
+
+        Map<String, String> properties = node.getDependency().getArtifact().getProperties();
+        assertNotNull( properties );
+        assertEquals( 2, properties.size() );
+
+        assertTrue( properties.containsKey( "test" ) );
+        assertEquals( "foo", properties.get( "test" ) );
+        assertTrue( properties.containsKey( "test2" ) );
+        assertEquals( "fizzle", properties.get( "test2" ) );
+    }
+
+    @Test
+    public void testSubstitutions()
+        throws IOException
+    {
+        parser.setSubstitutions( Arrays.asList( "subst1", "subst2" ) );
+        String def = "%s:%s:ext:ver";
+        DependencyNode root = parser.parseLiteral( def );
+        Artifact artifact = root.getDependency().getArtifact();
+        assertEquals( "subst2", artifact.getArtifactId() );
+        assertEquals( "subst1", artifact.getGroupId() );
+
+        def = "%s:aid:ext:ver\n\\- %s:aid:ext:ver";
+        root = parser.parseLiteral( def );
+
+        assertEquals( "subst1", root.getDependency().getArtifact().getGroupId() );
+        assertEquals( "subst2", root.getChildren().get( 0 ).getDependency().getArtifact().getGroupId() );
+    }
+
+    @Test
+    public void testMultiple()
+        throws IOException
+    {
+        String prefix = "org/eclipse/aether/internal/test/util/";
+        String name = "testResourceLoading.txt";
+
+        List<DependencyNode> nodes = parser.parseMultiResource( prefix + name );
+
+        assertEquals( 2, nodes.size() );
+        assertEquals( "aid", nodes.get( 0 ).getDependency().getArtifact().getArtifactId() );
+        assertEquals( "aid2", nodes.get( 1 ).getDependency().getArtifact().getArtifactId() );
+    }
+
+    @Test
+    public void testRootNullDependency()
+        throws IOException
+    {
+        String literal = "(null)\n+- gid:aid:ext:ver";
+        DependencyNode root = parser.parseLiteral( literal );
+
+        assertNull( root.getDependency() );
+        assertEquals( 1, root.getChildren().size() );
+    }
+
+    @Test
+    public void testChildNullDependency()
+        throws IOException
+    {
+        String literal = "gid:aid:ext:ver\n+- (null)";
+        DependencyNode root = parser.parseLiteral( literal );
+
+        assertNotNull( root.getDependency() );
+        assertEquals( 1, root.getChildren().size() );
+        assertNull( root.getChildren().get( 0 ).getDependency() );
+    }
+
+    @Test
+    public void testOptional()
+        throws IOException
+    {
+        String def = "gid:aid:jar:1 compile optional";
+
+        DependencyNode node = parser.parseLiteral( def );
+
+        assertNotNull( node );
+        assertEquals( 0, node.getChildren().size() );
+
+        Dependency dependency = node.getDependency();
+        assertNotNull( dependency );
+        assertEquals( "compile", dependency.getScope() );
+        assertEquals( true, dependency.isOptional() );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDataReaderTest.java b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDataReaderTest.java
new file mode 100644
index 0000000..4864b32
--- /dev/null
+++ b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDataReaderTest.java
@@ -0,0 +1,232 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.internal.test.util.ArtifactDescription;
+import org.eclipse.aether.internal.test.util.IniArtifactDataReader;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class IniArtifactDataReaderTest
+{
+
+    private IniArtifactDataReader parser;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        this.parser = new IniArtifactDataReader( "org/eclipse/aether/internal/test/util/" );
+    }
+
+    @Test
+    public void testRelocation()
+        throws IOException
+    {
+        String def = "[relocation]\ngid:aid:ext:ver";
+
+        ArtifactDescription description = parser.parseLiteral( def );
+
+        Artifact artifact = description.getRelocation();
+        assertNotNull( artifact );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "ver", artifact.getVersion() );
+        assertEquals( "ext", artifact.getExtension() );
+    }
+
+    @Test
+    public void testDependencies()
+        throws IOException
+    {
+        String def = "[dependencies]\ngid:aid:ext:ver\n-exclusion:aid\ngid2:aid2:ext2:ver2";
+
+        ArtifactDescription description = parser.parseLiteral( def );
+
+        List<Dependency> dependencies = description.getDependencies();
+        assertNotNull( dependencies );
+        assertEquals( 2, dependencies.size() );
+
+        Dependency dependency = dependencies.get( 0 );
+        assertEquals( "compile", dependency.getScope() );
+
+        Artifact artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "ver", artifact.getVersion() );
+        assertEquals( "ext", artifact.getExtension() );
+
+        Collection<Exclusion> exclusions = dependency.getExclusions();
+        assertNotNull( exclusions );
+        assertEquals( 1, exclusions.size() );
+        Exclusion exclusion = exclusions.iterator().next();
+        assertEquals( "exclusion", exclusion.getGroupId() );
+        assertEquals( "aid", exclusion.getArtifactId() );
+        assertEquals( "*", exclusion.getClassifier() );
+        assertEquals( "*", exclusion.getExtension() );
+
+        dependency = dependencies.get( 1 );
+
+        artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+        assertEquals( "aid2", artifact.getArtifactId() );
+        assertEquals( "gid2", artifact.getGroupId() );
+        assertEquals( "ver2", artifact.getVersion() );
+        assertEquals( "ext2", artifact.getExtension() );
+    }
+
+    @Test
+    public void testManagedDependencies()
+        throws IOException
+    {
+        String def = "[managed-dependencies]\ngid:aid:ext:ver\n-exclusion:aid\ngid2:aid2:ext2:ver2:runtime";
+
+        ArtifactDescription description = parser.parseLiteral( def );
+
+        List<Dependency> dependencies = description.getManagedDependencies();
+        assertNotNull( dependencies );
+        assertEquals( 2, dependencies.size() );
+
+        Dependency dependency = dependencies.get( 0 );
+        assertEquals( "", dependency.getScope() );
+
+        Artifact artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "ver", artifact.getVersion() );
+        assertEquals( "ext", artifact.getExtension() );
+
+        Collection<Exclusion> exclusions = dependency.getExclusions();
+        assertNotNull( exclusions );
+        assertEquals( 1, exclusions.size() );
+        Exclusion exclusion = exclusions.iterator().next();
+        assertEquals( "exclusion", exclusion.getGroupId() );
+        assertEquals( "aid", exclusion.getArtifactId() );
+        assertEquals( "*", exclusion.getClassifier() );
+        assertEquals( "*", exclusion.getExtension() );
+
+        dependency = dependencies.get( 1 );
+        assertEquals( "runtime", dependency.getScope() );
+
+        artifact = dependency.getArtifact();
+        assertNotNull( artifact );
+        assertEquals( "aid2", artifact.getArtifactId() );
+        assertEquals( "gid2", artifact.getGroupId() );
+        assertEquals( "ver2", artifact.getVersion() );
+        assertEquals( "ext2", artifact.getExtension() );
+
+        assertEquals( 0, dependency.getExclusions().size() );
+    }
+
+    @Test
+    public void testResource()
+        throws IOException
+    {
+        ArtifactDescription description = parser.parse( "ArtifactDataReaderTest.ini" );
+
+        Artifact artifact = description.getRelocation();
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "ver", artifact.getVersion() );
+        assertEquals( "ext", artifact.getExtension() );
+
+        assertEquals( 1, description.getRepositories().size() );
+        RemoteRepository repo = description.getRepositories().get( 0 );
+        assertEquals( "id", repo.getId() );
+        assertEquals( "type", repo.getContentType() );
+        assertEquals( "protocol://some/url?for=testing", repo.getUrl() );
+
+        assertDependencies( description.getDependencies() );
+        assertDependencies( description.getManagedDependencies() );
+
+    }
+
+    private void assertDependencies( List<Dependency> deps )
+    {
+        assertEquals( 4, deps.size() );
+
+        Dependency dep = deps.get( 0 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( false, dep.isOptional() );
+        assertEquals( 2, dep.getExclusions().size() );
+        Iterator<Exclusion> it = dep.getExclusions().iterator();
+        Exclusion excl = it.next();
+        assertEquals( "gid3", excl.getGroupId() );
+        assertEquals( "aid", excl.getArtifactId() );
+        excl = it.next();
+        assertEquals( "gid2", excl.getGroupId() );
+        assertEquals( "aid2", excl.getArtifactId() );
+
+        Artifact art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 1 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid2", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 2 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver3", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 3 );
+        assertEquals( "scope5", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid1", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReaderTest.java b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReaderTest.java
new file mode 100644
index 0000000..8b6bfa4
--- /dev/null
+++ b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/IniArtifactDescriptorReaderTest.java
@@ -0,0 +1,152 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.internal.test.util.IniArtifactDescriptorReader;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class IniArtifactDescriptorReaderTest
+{
+
+    private IniArtifactDescriptorReader reader;
+
+    private RepositorySystemSession session;
+
+    @Before
+    public void setup()
+        throws IOException
+    {
+        reader = new IniArtifactDescriptorReader( "org/eclipse/aether/internal/test/util/" );
+        session = TestUtils.newSession();
+    }
+
+    @Test( expected = ArtifactDescriptorException.class )
+    public void testMissingDescriptor()
+        throws ArtifactDescriptorException
+    {
+        Artifact art = new DefaultArtifact( "missing:aid:ver:ext" );
+        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest( art, null, "" );
+        reader.readArtifactDescriptor( session, request );
+    }
+
+    @Test
+    public void testLookup()
+        throws ArtifactDescriptorException
+    {
+        Artifact art = new DefaultArtifact( "gid:aid:ext:ver" );
+        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest( art, null, "" );
+        ArtifactDescriptorResult description = reader.readArtifactDescriptor( session, request );
+
+        assertEquals( request, description.getRequest() );
+        assertEquals( art.setVersion( "1" ), description.getArtifact() );
+
+        assertEquals( 1, description.getRelocations().size() );
+        Artifact artifact = description.getRelocations().get( 0 );
+        assertEquals( "gid", artifact.getGroupId() );
+        assertEquals( "aid", artifact.getArtifactId() );
+        assertEquals( "ver", artifact.getVersion() );
+        assertEquals( "ext", artifact.getExtension() );
+
+        assertEquals( 1, description.getRepositories().size() );
+        RemoteRepository repo = description.getRepositories().get( 0 );
+        assertEquals( "id", repo.getId() );
+        assertEquals( "type", repo.getContentType() );
+        assertEquals( "protocol://some/url?for=testing", repo.getUrl() );
+
+        assertDependencies( description.getDependencies() );
+        assertDependencies( description.getManagedDependencies() );
+
+    }
+
+    private void assertDependencies( List<Dependency> deps )
+    {
+        assertEquals( 4, deps.size() );
+
+        Dependency dep = deps.get( 0 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( false, dep.isOptional() );
+        assertEquals( 2, dep.getExclusions().size() );
+        Iterator<Exclusion> it = dep.getExclusions().iterator();
+        Exclusion excl = it.next();
+        assertEquals( "gid3", excl.getGroupId() );
+        assertEquals( "aid", excl.getArtifactId() );
+        excl = it.next();
+        assertEquals( "gid2", excl.getGroupId() );
+        assertEquals( "aid2", excl.getArtifactId() );
+
+        Artifact art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 1 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid2", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 2 );
+        assertEquals( "scope", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver3", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+
+        dep = deps.get( 3 );
+        assertEquals( "scope5", dep.getScope() );
+        assertEquals( true, dep.isOptional() );
+        assertEquals( 0, dep.getExclusions().size() );
+
+        art = dep.getArtifact();
+        assertEquals( "gid1", art.getGroupId() );
+        assertEquals( "aid", art.getArtifactId() );
+        assertEquals( "ver", art.getVersion() );
+        assertEquals( "ext", art.getExtension() );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/NodeDefinitionTest.java b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/NodeDefinitionTest.java
new file mode 100644
index 0000000..8f41a5a
--- /dev/null
+++ b/maven-resolver-test-util/src/test/java/org/eclipse/aether/internal/test/util/NodeDefinitionTest.java
@@ -0,0 +1,156 @@
+package org.eclipse.aether.internal.test.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+
+public class NodeDefinitionTest
+{
+
+    private void assertMatch( String text, String regex, String... groups )
+    {
+        Pattern pattern = Pattern.compile( regex );
+        Matcher matcher = pattern.matcher( text );
+        assertEquals( true, matcher.matches() );
+        assertTrue( groups.length + " vs " + matcher.groupCount(), groups.length <= matcher.groupCount() );
+        for ( int i = 1; i <= groups.length; i++ )
+        {
+            assertEquals( "Mismatch for group " + i, groups[i - 1], matcher.group( i ) );
+        }
+    }
+
+    private void assertNoMatch( String text, String regex )
+    {
+        Pattern pattern = Pattern.compile( regex );
+        Matcher matcher = pattern.matcher( text );
+        assertEquals( false, matcher.matches() );
+    }
+
+    @Test
+    public void testPatterns()
+    {
+        assertMatch( "(Example-ID_0123456789)", NodeDefinition.ID, "Example-ID_0123456789" );
+        assertMatch( "^Example-ID_0123456789", NodeDefinition.IDREF, "Example-ID_0123456789" );
+
+        assertMatch( "gid:aid:1", NodeDefinition.COORDS, "gid", "aid", null, null, "1" );
+        assertMatch( "gid:aid:jar:1", NodeDefinition.COORDS, "gid", "aid", "jar", null, "1" );
+        assertMatch( "gid:aid:jar:cls:1", NodeDefinition.COORDS, "gid", "aid", "jar", "cls", "1" );
+
+        assertMatch( "[1]", NodeDefinition.RANGE, "[1]" );
+        assertMatch( "[1,)", NodeDefinition.RANGE, "[1,)" );
+        assertMatch( "(1,2)", NodeDefinition.RANGE, "(1,2)" );
+
+        assertMatch( "scope  =  compile", NodeDefinition.SCOPE, "compile", null );
+        assertMatch( "scope=compile<runtime", NodeDefinition.SCOPE, "compile", "runtime" );
+        assertMatch( "compile<runtime", NodeDefinition.SCOPE, "compile", "runtime" );
+        assertNoMatch( "optional", NodeDefinition.SCOPE );
+        assertNoMatch( "!optional", NodeDefinition.SCOPE );
+
+        assertMatch( "optional", NodeDefinition.OPTIONAL, "optional" );
+        assertMatch( "!optional", NodeDefinition.OPTIONAL, "!optional" );
+
+        assertMatch( "relocations  =  g:a:1", NodeDefinition.RELOCATIONS, "g:a:1" );
+        assertMatch( "relocations=g:a:1 , g:a:2", NodeDefinition.RELOCATIONS, "g:a:1 , g:a:2" );
+
+        assertMatch( "props  =  Key:Value", NodeDefinition.PROPS, "Key:Value" );
+        assertMatch( "props=k:1 , k_2:v_2", NodeDefinition.PROPS, "k:1 , k_2:v_2" );
+
+        assertMatch( "gid:aid:1", NodeDefinition.COORDSX, "gid:aid:1", null, null );
+        assertMatch( "gid:aid:1[1,2)", NodeDefinition.COORDSX, "gid:aid:1", "[1,2)", null );
+        assertMatch( "gid:aid:1<2", NodeDefinition.COORDSX, "gid:aid:1", null, "2" );
+        assertMatch( "gid:aid:1(, 2)<[1, 3]", NodeDefinition.COORDSX, "gid:aid:1", "(, 2)", "[1, 3]" );
+
+        assertMatch( "gid:aid:1(, 2)<[1, 3] props=k:v scope=c<r optional relocations=g:a:v (id)", NodeDefinition.NODE,
+                     "gid:aid:1", "(, 2)", "[1, 3]", "k:v", "c", "r", "optional", "g:a:v", "id" );
+
+        assertMatch( "gid:aid:1(, 2)<[1, 3] props=k:v c<r optional relocations=g:a:v (id)", NodeDefinition.LINE, null,
+                     "gid:aid:1", "(, 2)", "[1, 3]", "k:v", "c", "r", "optional", "g:a:v", "id" );
+        assertMatch( "^id", NodeDefinition.LINE, "id", null, null, null );
+    }
+
+    @Test
+    public void testParsing_Reference()
+    {
+        NodeDefinition desc = new NodeDefinition( "^id" );
+        assertEquals( "id", desc.reference );
+    }
+
+    @Test
+    public void testParsing_Node()
+    {
+        NodeDefinition desc = new NodeDefinition( "g:a:1" );
+        assertEquals( null, desc.reference );
+        assertEquals( "g:a:1", desc.coords );
+        assertEquals( null, desc.range );
+        assertEquals( null, desc.premanagedVersion );
+        assertEquals( null, desc.scope );
+        assertEquals( null, desc.premanagedScope );
+        assertEquals( false, desc.optional );
+        assertEquals( null, desc.properties );
+        assertEquals( null, desc.relocations );
+        assertEquals( null, desc.id );
+
+        desc = new NodeDefinition( "gid1:aid1:ext1:ver1 scope1 !optional" );
+        assertEquals( null, desc.reference );
+        assertEquals( "gid1:aid1:ext1:ver1", desc.coords );
+        assertEquals( null, desc.range );
+        assertEquals( null, desc.premanagedVersion );
+        assertEquals( "scope1", desc.scope );
+        assertEquals( null, desc.premanagedScope );
+        assertEquals( false, desc.optional );
+        assertEquals( null, desc.properties );
+        assertEquals( null, desc.relocations );
+        assertEquals( null, desc.id );
+
+        desc = new NodeDefinition( "g:a:1 optional" );
+        assertEquals( null, desc.reference );
+        assertEquals( "g:a:1", desc.coords );
+        assertEquals( null, desc.range );
+        assertEquals( null, desc.premanagedVersion );
+        assertEquals( null, desc.scope );
+        assertEquals( null, desc.premanagedScope );
+        assertEquals( true, desc.optional );
+        assertEquals( null, desc.properties );
+        assertEquals( null, desc.relocations );
+        assertEquals( null, desc.id );
+
+        desc =
+            new NodeDefinition( "gid:aid:1(, 2)<[1, 3]" + " props = k:v" + " scope=c<r" + " optional"
+                + " relocations = g:a:v , g:a:1" + " (id)" );
+        assertEquals( null, desc.reference );
+        assertEquals( "gid:aid:1", desc.coords );
+        assertEquals( "(, 2)", desc.range );
+        assertEquals( "[1, 3]", desc.premanagedVersion );
+        assertEquals( "c", desc.scope );
+        assertEquals( "r", desc.premanagedScope );
+        assertEquals( true, desc.optional );
+        assertEquals( Collections.singletonMap( "k", "v" ), desc.properties );
+        assertEquals( Arrays.asList( "g:a:v", "g:a:1" ), desc.relocations );
+        assertEquals( "id", desc.id );
+    }
+
+}
diff --git a/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/ArtifactDataReaderTest.ini b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/ArtifactDataReaderTest.ini
new file mode 100644
index 0000000..28e3634
--- /dev/null
+++ b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/ArtifactDataReaderTest.ini
@@ -0,0 +1,21 @@
+[relocation]
+gid:aid:ext:ver
+
+[dependencies]
+gid:aid:ext:ver:scope
+-gid3:aid
+-gid2:aid2
+gid:aid2:ext:ver:scope:optional
+gid:aid:ext:ver3:scope:optional
+gid1:aid:ext:ver:scope5:optional
+
+[managedDependencies]
+gid:aid:ext:ver:scope
+-gid3:aid
+-gid2:aid2
+gid:aid2:ext:ver:scope:optional
+gid:aid:ext:ver3:scope:optional
+gid1:aid:ext:ver:scope5:optional
+
+[repositories]
+id:type:protocol://some/url?for=testing
diff --git a/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_1.ini b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_1.ini
new file mode 100644
index 0000000..1373b19
--- /dev/null
+++ b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_1.ini
@@ -0,0 +1,18 @@
+[dependencies]
+gid:aid:ext:ver:scope
+-gid3:aid
+-gid2:aid2
+gid:aid2:ext:ver:scope:optional
+gid:aid:ext:ver3:scope:optional
+gid1:aid:ext:ver:scope5:optional
+
+[managedDependencies]
+gid:aid:ext:ver:scope
+-gid3:aid
+-gid2:aid2
+gid:aid2:ext:ver:scope:optional
+gid:aid:ext:ver3:scope:optional
+gid1:aid:ext:ver:scope5:optional
+
+[repositories]
+id:type:protocol://some/url?for=testing
diff --git a/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_ver.ini b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_ver.ini
new file mode 100644
index 0000000..7f45908
--- /dev/null
+++ b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/gid_aid_ver.ini
@@ -0,0 +1,2 @@
+[relocation]
+gid:aid:ext:1
diff --git a/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/testResourceLoading.txt b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/testResourceLoading.txt
new file mode 100644
index 0000000..6c79ca5
--- /dev/null
+++ b/maven-resolver-test-util/src/test/resources/org/eclipse/aether/internal/test/util/testResourceLoading.txt
@@ -0,0 +1,3 @@
+gid:aid:ext:ver
+---
+gid:aid2:ext:ver
\ No newline at end of file
diff --git a/maven-resolver-transport-classpath/pom.xml b/maven-resolver-transport-classpath/pom.xml
new file mode 100644
index 0000000..04d45f5
--- /dev/null
+++ b/maven-resolver-transport-classpath/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-transport-classpath</artifactId>
+
+  <name>Maven Artifact Resolver Transport Classpath</name>
+  <description>
+      A transport implementation for repositories using classpath:// URLs.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.transport.classpath</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporter.java b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporter.java
new file mode 100644
index 0000000..493c907
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporter.java
@@ -0,0 +1,149 @@
+package org.eclipse.aether.transport.classpath;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.TransportTask;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * A transporter reading from the classpath.
+ */
+final class ClasspathTransporter
+    extends AbstractTransporter
+{
+
+    private final String resourceBase;
+
+    private final ClassLoader classLoader;
+
+    public ClasspathTransporter( RepositorySystemSession session, RemoteRepository repository, Logger logger )
+        throws NoTransporterException
+    {
+        if ( !"classpath".equalsIgnoreCase( repository.getProtocol() ) )
+        {
+            throw new NoTransporterException( repository );
+        }
+
+        String base;
+        try
+        {
+            URI uri = new URI( repository.getUrl() );
+            String ssp = uri.getSchemeSpecificPart();
+            if ( ssp.startsWith( "/" ) )
+            {
+                base = uri.getPath();
+                if ( base == null )
+                {
+                    base = "";
+                }
+                else if ( base.startsWith( "/" ) )
+                {
+                    base = base.substring( 1 );
+                }
+            }
+            else
+            {
+                base = ssp;
+            }
+            if ( base.length() > 0 && !base.endsWith( "/" ) )
+            {
+                base += '/';
+            }
+        }
+        catch ( URISyntaxException e )
+        {
+            throw new NoTransporterException( repository, e );
+        }
+        resourceBase = base;
+
+        Object cl = ConfigUtils.getObject( session, null, ClasspathTransporterFactory.CONFIG_PROP_CLASS_LOADER );
+        if ( cl instanceof ClassLoader )
+        {
+            classLoader = (ClassLoader) cl;
+        }
+        else
+        {
+            classLoader = Thread.currentThread().getContextClassLoader();
+        }
+    }
+
+    private URL getResource( TransportTask task )
+        throws Exception
+    {
+        String resource = resourceBase + task.getLocation().getPath();
+        URL url = classLoader.getResource( resource );
+        if ( url == null )
+        {
+            throw new ResourceNotFoundException( "Could not locate " + resource );
+        }
+        return url;
+    }
+
+    public int classify( Throwable error )
+    {
+        if ( error instanceof ResourceNotFoundException )
+        {
+            return ERROR_NOT_FOUND;
+        }
+        return ERROR_OTHER;
+    }
+
+    @Override
+    protected void implPeek( PeekTask task )
+        throws Exception
+    {
+        getResource( task );
+    }
+
+    @Override
+    protected void implGet( GetTask task )
+        throws Exception
+    {
+        URL url = getResource( task );
+        URLConnection conn = url.openConnection();
+        utilGet( task, conn.getInputStream(), true, conn.getContentLength(), false );
+    }
+
+    @Override
+    protected void implPut( PutTask task )
+        throws Exception
+    {
+        throw new UnsupportedOperationException( "Uploading to a classpath: repository is not supported" );
+    }
+
+    @Override
+    protected void implClose()
+    {
+    }
+
+}
diff --git a/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporterFactory.java b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporterFactory.java
new file mode 100644
index 0000000..18db417
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ClasspathTransporterFactory.java
@@ -0,0 +1,116 @@
+package org.eclipse.aether.transport.classpath;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A transporter factory for repositories using the {@code classpath:} protocol. As example, getting an item named
+ * {@code some/file.txt} from a repository with the URL {@code classpath:/base/dir} results in retrieving the resource
+ * {@code base/dir/some/file.txt} from the classpath. The classpath to load the resources from is given via a
+ * {@link ClassLoader} that can be configured via the configuration property {@link #CONFIG_PROP_CLASS_LOADER}.
+ * <p>
+ * <em>Note:</em> Such repositories are read-only and uploads to them are generally not supported.
+ */
+@Named( "classpath" )
+public final class ClasspathTransporterFactory
+    implements TransporterFactory, Service
+{
+
+    /**
+     * The key in the repository session's {@link RepositorySystemSession#getConfigProperties() configuration
+     * properties} used to store a {@link ClassLoader} from which resources should be retrieved. If unspecified, the
+     * {@link Thread#getContextClassLoader() context class loader} of the current thread will be used.
+     */
+    public static final String CONFIG_PROP_CLASS_LOADER = "aether.connector.classpath.loader";
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private float priority;
+
+    /**
+     * Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation
+     * by clients, the new factory needs to be configured via its various mutators before first use or runtime errors
+     * will occur.
+     */
+    public ClasspathTransporterFactory()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    ClasspathTransporterFactory( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    /**
+     * Sets the logger factory to use for this component.
+     * 
+     * @param loggerFactory The logger factory to use, may be {@code null} to disable logging.
+     * @return This component for chaining, never {@code null}.
+     */
+    public ClasspathTransporterFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, ClasspathTransporter.class );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public ClasspathTransporterFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException
+    {
+        return new ClasspathTransporter( session, repository, logger );
+    }
+
+}
diff --git a/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ResourceNotFoundException.java b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ResourceNotFoundException.java
new file mode 100644
index 0000000..d30a0ff
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/ResourceNotFoundException.java
@@ -0,0 +1,37 @@
+package org.eclipse.aether.transport.classpath;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+
+/**
+ * Special exception type used instead of {@code FileNotFoundException} to avoid misinterpretation of errors unrelated
+ * to the remote resource.
+ */
+class ResourceNotFoundException
+    extends IOException
+{
+
+    public ResourceNotFoundException( String message )
+    {
+        super( message );
+    }
+
+}
diff --git a/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/package-info.java b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/package-info.java
new file mode 100644
index 0000000..8bcda93
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/main/java/org/eclipse/aether/transport/classpath/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Support for downloads that utilize the classpath as "remote" storage.
+ */
+package org.eclipse.aether.transport.classpath;
+
diff --git a/maven-resolver-transport-classpath/src/site/site.xml b/maven-resolver-transport-classpath/src/site/site.xml
new file mode 100644
index 0000000..abeaa3a
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Transport Classpath">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/ClasspathTransporterTest.java b/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/ClasspathTransporterTest.java
new file mode 100644
index 0000000..0f7647c
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/ClasspathTransporterTest.java
@@ -0,0 +1,412 @@
+package org.eclipse.aether.transport.classpath;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class ClasspathTransporterTest
+{
+
+    private DefaultRepositorySystemSession session;
+
+    private TransporterFactory factory;
+
+    private Transporter transporter;
+
+    private RemoteRepository newRepo( String url )
+    {
+        return new RemoteRepository.Builder( "test", "default", url ).build();
+    }
+
+    private void newTransporter( String url )
+        throws Exception
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        transporter = factory.newInstance( session, newRepo( url ) );
+    }
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        factory = new ClasspathTransporterFactory( new TestLoggerFactory() );
+        newTransporter( "classpath:/repository" );
+    }
+
+    @After
+    public void tearDown()
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        factory = null;
+        session = null;
+    }
+
+    @Test
+    public void testClassify()
+        throws Exception
+    {
+        assertEquals( Transporter.ERROR_OTHER, transporter.classify( new FileNotFoundException() ) );
+        assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new ResourceNotFoundException( "test" ) ) );
+    }
+
+    @Test
+    public void testPeek()
+        throws Exception
+    {
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceNotFoundException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_ToMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ToFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "test", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EmptyResource()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "empty.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EncodedResourcePath()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "some%20space.txt" ) );
+        transporter.get( task );
+        assertEquals( "space", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_Fragment()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "file.txt#ignored" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_Query()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "file.txt?ignored" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File file = TestFileUtils.createTempFile( "failure" );
+            transporter.get( new GetTask( URI.create( "file.txt" ) ).setDataFile( file ) );
+            assertTrue( i + ", " + file.getAbsolutePath(), file.delete() );
+        }
+    }
+
+    @Test
+    public void testGet_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.get( new GetTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceNotFoundException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.get( new GetTask( URI.create( "file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testGet_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut()
+        throws Exception
+    {
+        try
+        {
+            transporter.put( new PutTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( UnsupportedOperationException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPut_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.put( new PutTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test( expected = NoTransporterException.class )
+    public void testInit_BadProtocol()
+        throws Exception
+    {
+        newTransporter( "bad:/void" );
+    }
+
+    @Test
+    public void testInit_CaseInsensitiveProtocol()
+        throws Exception
+    {
+        newTransporter( "classpath:/void" );
+        newTransporter( "CLASSPATH:/void" );
+        newTransporter( "ClassPath:/void" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrl()
+        throws Exception
+    {
+        testInit( "classpath:repository" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrlTrailingSlash()
+        throws Exception
+    {
+        testInit( "classpath:repository/" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrlSpaces()
+        throws Exception
+    {
+        testInit( "classpath:repo%20space" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrl()
+        throws Exception
+    {
+        testInit( "classpath:/repository" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlTrailingSlash()
+        throws Exception
+    {
+        testInit( "classpath:/repository/" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlSpaces()
+        throws Exception
+    {
+        testInit( "classpath:/repo%20space" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlRoot()
+        throws Exception
+    {
+        testInit( "classpath:/" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlNoPath()
+        throws Exception
+    {
+        testInit( "classpath://reserved" );
+    }
+
+    private void testInit( String base )
+        throws Exception
+    {
+        newTransporter( base );
+        GetTask task = new GetTask( URI.create( "file.txt" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+    }
+
+}
diff --git a/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/RecordingTransportListener.java b/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/RecordingTransportListener.java
new file mode 100644
index 0000000..9447d39
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/java/org/eclipse/aether/transport/classpath/RecordingTransportListener.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.transport.classpath;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+class RecordingTransportListener
+    extends TransportListener
+{
+
+    public final ByteArrayOutputStream baos = new ByteArrayOutputStream( 1024 );
+
+    public long dataOffset;
+
+    public long dataLength;
+
+    public int startedCount;
+
+    public int progressedCount;
+
+    public boolean cancelStart;
+
+    public boolean cancelProgress;
+
+    @Override
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+        startedCount++;
+        progressedCount = 0;
+        this.dataLength = dataLength;
+        this.dataOffset = dataOffset;
+        baos.reset();
+        if ( cancelStart )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+    @Override
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+        progressedCount++;
+        baos.write( data.array(), data.arrayOffset() + data.position(), data.remaining() );
+        if ( cancelProgress )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-classpath/src/test/resources/file.txt b/maven-resolver-transport-classpath/src/test/resources/file.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/resources/file.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/maven-resolver-transport-classpath/src/test/resources/repo space/file.txt b/maven-resolver-transport-classpath/src/test/resources/repo space/file.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/resources/repo space/file.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/maven-resolver-transport-classpath/src/test/resources/repository/empty.txt b/maven-resolver-transport-classpath/src/test/resources/repository/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/resources/repository/empty.txt
diff --git a/maven-resolver-transport-classpath/src/test/resources/repository/file.txt b/maven-resolver-transport-classpath/src/test/resources/repository/file.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/resources/repository/file.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/maven-resolver-transport-classpath/src/test/resources/repository/some space.txt b/maven-resolver-transport-classpath/src/test/resources/repository/some space.txt
new file mode 100644
index 0000000..82cbe04
--- /dev/null
+++ b/maven-resolver-transport-classpath/src/test/resources/repository/some space.txt
@@ -0,0 +1 @@
+space
\ No newline at end of file
diff --git a/maven-resolver-transport-file/pom.xml b/maven-resolver-transport-file/pom.xml
new file mode 100644
index 0000000..50d3730
--- /dev/null
+++ b/maven-resolver-transport-file/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-transport-file</artifactId>
+
+  <name>Maven Artifact Resolver Transport File</name>
+  <description>
+      A transport implementation for repositories using file:// URLs.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.transport.file</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java
new file mode 100644
index 0000000..02286c8
--- /dev/null
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.TransportTask;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A transporter using {@link java.io.File}.
+ */
+final class FileTransporter
+    extends AbstractTransporter
+{
+
+    private final Logger logger;
+
+    private final File basedir;
+
+    public FileTransporter( RemoteRepository repository, Logger logger )
+        throws NoTransporterException
+    {
+        if ( !"file".equalsIgnoreCase( repository.getProtocol() ) )
+        {
+            throw new NoTransporterException( repository );
+        }
+        this.logger = logger;
+        basedir = new File( PathUtils.basedir( repository.getUrl() ) ).getAbsoluteFile();
+    }
+
+    File getBasedir()
+    {
+        return basedir;
+    }
+
+    public int classify( Throwable error )
+    {
+        if ( error instanceof ResourceNotFoundException )
+        {
+            return ERROR_NOT_FOUND;
+        }
+        return ERROR_OTHER;
+    }
+
+    @Override
+    protected void implPeek( PeekTask task )
+        throws Exception
+    {
+        getFile( task, true );
+    }
+
+    @Override
+    protected void implGet( GetTask task )
+        throws Exception
+    {
+        File file = getFile( task, true );
+        utilGet( task, new FileInputStream( file ), true, file.length(), false );
+    }
+
+    @Override
+    protected void implPut( PutTask task )
+        throws Exception
+    {
+        File file = getFile( task, false );
+        file.getParentFile().mkdirs();
+        try
+        {
+            utilPut( task, new FileOutputStream( file ), true );
+        }
+        catch ( Exception e )
+        {
+            if ( !file.delete() && file.exists() )
+            {
+                logger.debug( "Could not delete partial file " + file );
+            }
+            throw e;
+        }
+    }
+
+    private File getFile( TransportTask task, boolean required )
+        throws Exception
+    {
+        String path = task.getLocation().getPath();
+        if ( path.contains( "../" ) )
+        {
+            throw new IllegalArgumentException( "illegal resource path: " + path );
+        }
+        File file = new File( basedir, path );
+        if ( required && !file.exists() )
+        {
+            throw new ResourceNotFoundException( "Could not locate " + file );
+        }
+        return file;
+    }
+
+    @Override
+    protected void implClose()
+    {
+    }
+
+}
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java
new file mode 100644
index 0000000..86ae6fc
--- /dev/null
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java
@@ -0,0 +1,104 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A transporter factory for repositories using the {@code file:} protocol.
+ */
+@Named( "file" )
+public final class FileTransporterFactory
+    implements TransporterFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private float priority;
+
+    /**
+     * Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation
+     * by clients, the new factory needs to be configured via its various mutators before first use or runtime errors
+     * will occur.
+     */
+    public FileTransporterFactory()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    FileTransporterFactory( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    /**
+     * Sets the logger factory to use for this component.
+     * 
+     * @param loggerFactory The logger factory to use, may be {@code null} to disable logging.
+     * @return This component for chaining, never {@code null}.
+     */
+    public FileTransporterFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, FileTransporter.class );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public FileTransporterFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException
+    {
+        return new FileTransporter( repository, logger );
+    }
+
+}
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java
new file mode 100644
index 0000000..ac3f8fd
--- /dev/null
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java
@@ -0,0 +1,138 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+/**
+ * URL handling for file URLs. Based on org.apache.maven.wagon.PathUtils.
+ */
+final class PathUtils
+{
+
+    private PathUtils()
+    {
+    }
+
+    /**
+     * Return the protocol name. <br/>
+     * E.g: for input <code>http://www.codehause.org</code> this method will return <code>http</code>
+     * 
+     * @param url the url
+     * @return the host name
+     */
+    public static String protocol( final String url )
+    {
+        final int pos = url.indexOf( ":" );
+
+        if ( pos == -1 )
+        {
+            return "";
+        }
+        return url.substring( 0, pos ).trim();
+    }
+
+    /**
+     * Derive the path portion of the given URL.
+     * 
+     * @param url the file-repository URL
+     * @return the basedir of the repository
+     */
+    public static String basedir( String url )
+    {
+        String protocol = PathUtils.protocol( url );
+
+        String retValue = null;
+
+        if ( protocol.length() > 0 )
+        {
+            retValue = url.substring( protocol.length() + 1 );
+        }
+        else
+        {
+            retValue = url;
+        }
+        retValue = decode( retValue );
+        // special case: if omitted // on protocol, keep path as is
+        if ( retValue.startsWith( "//" ) )
+        {
+            retValue = retValue.substring( 2 );
+
+            if ( retValue.length() >= 2 && ( retValue.charAt( 1 ) == '|' || retValue.charAt( 1 ) == ':' ) )
+            {
+                // special case: if there is a windows drive letter, then keep the original return value
+                retValue = retValue.charAt( 0 ) + ":" + retValue.substring( 2 );
+            }
+            else
+            {
+                // Now we expect the host
+                int index = retValue.indexOf( "/" );
+                if ( index >= 0 )
+                {
+                    retValue = retValue.substring( index + 1 );
+                }
+
+                // special case: if there is a windows drive letter, then keep the original return value
+                if ( retValue.length() >= 2 && ( retValue.charAt( 1 ) == '|' || retValue.charAt( 1 ) == ':' ) )
+                {
+                    retValue = retValue.charAt( 0 ) + ":" + retValue.substring( 2 );
+                }
+                else if ( index >= 0 )
+                {
+                    // leading / was previously stripped
+                    retValue = "/" + retValue;
+                }
+            }
+        }
+
+        // special case: if there is a windows drive letter using |, switch to :
+        if ( retValue.length() >= 2 && retValue.charAt( 1 ) == '|' )
+        {
+            retValue = retValue.charAt( 0 ) + ":" + retValue.substring( 2 );
+        }
+
+        return retValue.trim();
+    }
+
+    /**
+     * Decodes the specified (portion of a) URL. <strong>Note:</strong> This decoder assumes that ISO-8859-1 is used to
+     * convert URL-encoded octets to characters.
+     * 
+     * @param url The URL to decode, may be <code>null</code>.
+     * @return The decoded URL or <code>null</code> if the input was <code>null</code>.
+     */
+    static String decode( String url )
+    {
+        String decoded = url;
+        if ( url != null )
+        {
+            int pos = -1;
+            while ( ( pos = decoded.indexOf( '%', pos + 1 ) ) >= 0 )
+            {
+                if ( pos + 2 < decoded.length() )
+                {
+                    String hexStr = decoded.substring( pos + 1, pos + 3 );
+                    char ch = (char) Integer.parseInt( hexStr, 16 );
+                    decoded = decoded.substring( 0, pos ) + ch + decoded.substring( pos + 3 );
+                }
+            }
+        }
+        return decoded;
+    }
+
+}
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/ResourceNotFoundException.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/ResourceNotFoundException.java
new file mode 100644
index 0000000..462fa0a
--- /dev/null
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/ResourceNotFoundException.java
@@ -0,0 +1,37 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+
+/**
+ * Special exception type used instead of {@code FileNotFoundException} to avoid misinterpretation of errors unrelated
+ * to the remote resource.
+ */
+class ResourceNotFoundException
+    extends IOException
+{
+
+    public ResourceNotFoundException( String message )
+    {
+        super( message );
+    }
+
+}
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java
new file mode 100644
index 0000000..8220bf4
--- /dev/null
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Support for downloads/uploads using the local filesystem as "remote" storage.
+ */
+package org.eclipse.aether.transport.file;
+
diff --git a/maven-resolver-transport-file/src/site/site.xml b/maven-resolver-transport-file/src/site/site.xml
new file mode 100644
index 0000000..916ba7a
--- /dev/null
+++ b/maven-resolver-transport-file/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Transport File">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java
new file mode 100644
index 0000000..dd65bf0
--- /dev/null
+++ b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java
@@ -0,0 +1,555 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class FileTransporterTest
+{
+
+    private DefaultRepositorySystemSession session;
+
+    private TransporterFactory factory;
+
+    private Transporter transporter;
+
+    private File repoDir;
+
+    private RemoteRepository newRepo( String url )
+    {
+        return new RemoteRepository.Builder( "test", "default", url ).build();
+    }
+
+    private void newTransporter( String url )
+        throws Exception
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        transporter = factory.newInstance( session, newRepo( url ) );
+    }
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        factory = new FileTransporterFactory( new TestLoggerFactory() );
+        repoDir = TestFileUtils.createTempDir();
+        TestFileUtils.writeString( new File( repoDir, "file.txt" ), "test" );
+        TestFileUtils.writeString( new File( repoDir, "empty.txt" ), "" );
+        TestFileUtils.writeString( new File( repoDir, "some space.txt" ), "space" );
+        newTransporter( repoDir.toURI().toString() );
+    }
+
+    @After
+    public void tearDown()
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        factory = null;
+        session = null;
+    }
+
+    @Test
+    public void testClassify()
+        throws Exception
+    {
+        assertEquals( Transporter.ERROR_OTHER, transporter.classify( new FileNotFoundException() ) );
+        assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new ResourceNotFoundException( "test" ) ) );
+    }
+
+    @Test
+    public void testPeek()
+        throws Exception
+    {
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceNotFoundException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_ToMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ToFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "test", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EmptyResource()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "empty.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EncodedResourcePath()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "some%20space.txt" ) );
+        transporter.get( task );
+        assertEquals( "space", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_Fragment()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "file.txt#ignored" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_Query()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "file.txt?ignored" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File file = TestFileUtils.createTempFile( "failure" );
+            transporter.get( new GetTask( URI.create( "file.txt" ) ).setDataFile( file ) );
+            assertTrue( i + ", " + file.getAbsolutePath(), file.delete() );
+        }
+    }
+
+    @Test
+    public void testGet_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.get( new GetTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceNotFoundException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.get( new GetTask( URI.create( "file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testGet_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_FromMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_FromFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "upload" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataFile( file );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_EmptyResource()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_NonExistentParentDir()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task =
+            new PutTask( URI.create( "dir/sub/dir/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "dir/sub/dir/file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_EncodedResourcePath()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "some%20space.txt" ) ).setListener( listener ).setDataString( "OK" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 2L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "OK", TestFileUtils.readString( new File( repoDir, "some space.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File src = TestFileUtils.createTempFile( "upload" );
+            File dst = new File( repoDir, "file.txt" );
+            transporter.put( new PutTask( URI.create( "file.txt" ) ).setDataFile( src ) );
+            assertTrue( i + ", " + src.getAbsolutePath(), src.delete() );
+            assertTrue( i + ", " + dst.getAbsolutePath(), dst.delete() );
+        }
+    }
+
+    @Test
+    public void testPut_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.put( new PutTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPut_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertFalse( new File( repoDir, "file.txt" ).exists() );
+    }
+
+    @Test
+    public void testPut_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+        assertFalse( new File( repoDir, "file.txt" ).exists() );
+    }
+
+    @Test( expected = NoTransporterException.class )
+    public void testInit_BadProtocol()
+        throws Exception
+    {
+        newTransporter( "bad:/void" );
+    }
+
+    @Test
+    public void testInit_CaseInsensitiveProtocol()
+        throws Exception
+    {
+        newTransporter( "file:/void" );
+        newTransporter( "FILE:/void" );
+        newTransporter( "File:/void" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrl()
+        throws Exception
+    {
+        testInit( "file:repository", "repository" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrlTrailingSlash()
+        throws Exception
+    {
+        testInit( "file:repository/", "repository" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrlSpaces()
+        throws Exception
+    {
+        testInit( "file:repo%20space", "repo space" );
+    }
+
+    @Test
+    public void testInit_OpaqueUrlSpacesDecoded()
+        throws Exception
+    {
+        testInit( "file:repo space", "repo space" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrl()
+        throws Exception
+    {
+        testInit( "file:/repository", "/repository" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlTrailingSlash()
+        throws Exception
+    {
+        testInit( "file:/repository/", "/repository" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlSpaces()
+        throws Exception
+    {
+        testInit( "file:/repo%20space", "/repo space" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlSpacesDecoded()
+        throws Exception
+    {
+        testInit( "file:/repo space", "/repo space" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlRoot()
+        throws Exception
+    {
+        testInit( "file:/", "/" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlHostNoPath()
+        throws Exception
+    {
+        testInit( "file://host/", "/" );
+    }
+
+    @Test
+    public void testInit_HierarchicalUrlHostPath()
+        throws Exception
+    {
+        testInit( "file://host/dir", "/dir" );
+    }
+
+    private void testInit( String base, String expected )
+        throws Exception
+    {
+        newTransporter( base );
+        File exp = new File( expected ).getAbsoluteFile();
+        assertEquals( exp, ( (FileTransporter) transporter ).getBasedir() );
+    }
+
+}
diff --git a/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/RecordingTransportListener.java b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/RecordingTransportListener.java
new file mode 100644
index 0000000..c6331e0
--- /dev/null
+++ b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/RecordingTransportListener.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.transport.file;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+class RecordingTransportListener
+    extends TransportListener
+{
+
+    public final ByteArrayOutputStream baos = new ByteArrayOutputStream( 1024 );
+
+    public long dataOffset;
+
+    public long dataLength;
+
+    public int startedCount;
+
+    public int progressedCount;
+
+    public boolean cancelStart;
+
+    public boolean cancelProgress;
+
+    @Override
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+        startedCount++;
+        progressedCount = 0;
+        this.dataLength = dataLength;
+        this.dataOffset = dataOffset;
+        baos.reset();
+        if ( cancelStart )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+    @Override
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+        progressedCount++;
+        baos.write( data.array(), data.arrayOffset() + data.position(), data.remaining() );
+        if ( cancelProgress )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-http/pom.xml b/maven-resolver-transport-http/pom.xml
new file mode 100644
index 0000000..4833732
--- /dev/null
+++ b/maven-resolver-transport-http/pom.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-transport-http</artifactId>
+
+  <name>Maven Artifact Resolver Transport HTTP</name>
+  <description>
+      A transport implementation for repositories using http:// and https:// URLs.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.transport.http</AutomaticModuleName>
+    <jettyVersion>9.2.9.v20150224</jettyVersion>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>4.5.3</version>
+      <exclusions>
+        <exclusion>
+          <!-- using jcl-over-slf4j instead -->
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpcore</artifactId>
+      <version>4.4.6</version>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>${slf4jVersion}</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>${jettyVersion}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-util</artifactId>
+      <version>${jettyVersion}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <version>${jettyVersion}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>${slf4jVersion}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/AuthSchemePool.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/AuthSchemePool.java
new file mode 100644
index 0000000..9b86252
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/AuthSchemePool.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.util.LinkedList;
+
+import org.apache.http.auth.AuthScheme;
+import org.apache.http.client.params.AuthPolicy;
+import org.apache.http.impl.auth.BasicScheme;
+
+/**
+ * Pool of (equivalent) auth schemes for a single host.
+ */
+final class AuthSchemePool
+{
+
+    private final LinkedList<AuthScheme> authSchemes;
+
+    private String schemeName;
+
+    public AuthSchemePool()
+    {
+        authSchemes = new LinkedList<AuthScheme>();
+    }
+
+    public synchronized AuthScheme get()
+    {
+        AuthScheme authScheme = null;
+        if ( !authSchemes.isEmpty() )
+        {
+            authScheme = authSchemes.removeLast();
+        }
+        else if ( AuthPolicy.BASIC.equalsIgnoreCase( schemeName ) )
+        {
+            authScheme = new BasicScheme();
+        }
+        return authScheme;
+    }
+
+    public synchronized void put( AuthScheme authScheme )
+    {
+        if ( authScheme == null )
+        {
+            return;
+        }
+        if ( !authScheme.getSchemeName().equals( schemeName ) )
+        {
+            schemeName = authScheme.getSchemeName();
+            authSchemes.clear();
+        }
+        authSchemes.add( authScheme );
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DeferredCredentialsProvider.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DeferredCredentialsProvider.java
new file mode 100644
index 0000000..c0daeaf
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DeferredCredentialsProvider.java
@@ -0,0 +1,194 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.NTCredentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.eclipse.aether.repository.AuthenticationContext;
+
+/**
+ * Credentials provider that defers calls into the auth context until authentication is actually requested.
+ */
+final class DeferredCredentialsProvider
+    implements CredentialsProvider
+{
+
+    private final CredentialsProvider delegate;
+
+    private final Map<AuthScope, Factory> factories;
+
+    public DeferredCredentialsProvider()
+    {
+        delegate = new BasicCredentialsProvider();
+        factories = new HashMap<AuthScope, Factory>();
+    }
+
+    public void setCredentials( AuthScope authScope, Factory factory )
+    {
+        factories.put( authScope, factory );
+    }
+
+    public void setCredentials( AuthScope authScope, Credentials credentials )
+    {
+        delegate.setCredentials( authScope, credentials );
+    }
+
+    public Credentials getCredentials( AuthScope authScope )
+    {
+        synchronized ( factories )
+        {
+            for ( Iterator<Map.Entry<AuthScope, Factory>> it = factories.entrySet().iterator(); it.hasNext(); )
+            {
+                Map.Entry<AuthScope, Factory> entry = it.next();
+                if ( authScope.match( entry.getKey() ) >= 0 )
+                {
+                    it.remove();
+                    delegate.setCredentials( entry.getKey(), entry.getValue().newCredentials() );
+                }
+            }
+        }
+        return delegate.getCredentials( authScope );
+    }
+
+    public void clear()
+    {
+        delegate.clear();
+    }
+
+    interface Factory
+    {
+
+        Credentials newCredentials();
+
+    }
+
+    static class BasicFactory
+        implements Factory
+    {
+
+        private final AuthenticationContext authContext;
+
+        public BasicFactory( AuthenticationContext authContext )
+        {
+            this.authContext = authContext;
+        }
+
+        public Credentials newCredentials()
+        {
+            String username = authContext.get( AuthenticationContext.USERNAME );
+            if ( username == null )
+            {
+                return null;
+            }
+            String password = authContext.get( AuthenticationContext.PASSWORD );
+            return new UsernamePasswordCredentials( username, password );
+        }
+
+    }
+
+    static class NtlmFactory
+        implements Factory
+    {
+
+        private final AuthenticationContext authContext;
+
+        public NtlmFactory( AuthenticationContext authContext )
+        {
+            this.authContext = authContext;
+        }
+
+        public Credentials newCredentials()
+        {
+            String username = authContext.get( AuthenticationContext.USERNAME );
+            if ( username == null )
+            {
+                return null;
+            }
+            String password = authContext.get( AuthenticationContext.PASSWORD );
+            String domain = authContext.get( AuthenticationContext.NTLM_DOMAIN );
+            String workstation = authContext.get( AuthenticationContext.NTLM_WORKSTATION );
+
+            if ( domain == null )
+            {
+                int backslash = username.indexOf( '\\' );
+                if ( backslash < 0 )
+                {
+                    domain = guessDomain();
+                }
+                else
+                {
+                    domain = username.substring( 0, backslash );
+                    username = username.substring( backslash + 1 );
+                }
+            }
+            if ( workstation == null )
+            {
+                workstation = guessWorkstation();
+            }
+
+            return new NTCredentials( username, password, workstation, domain );
+        }
+
+        private static String guessDomain()
+        {
+            return safeNtlmString( System.getProperty( "http.auth.ntlm.domain" ), System.getenv( "USERDOMAIN" ) );
+        }
+
+        private static String guessWorkstation()
+        {
+            String localHost = null;
+            try
+            {
+                localHost = InetAddress.getLocalHost().getHostName();
+            }
+            catch ( UnknownHostException e )
+            {
+                // well, we have other options to try
+            }
+            return safeNtlmString( System.getProperty( "http.auth.ntlm.host" ), System.getenv( "COMPUTERNAME" ),
+                                   localHost );
+        }
+
+        private static String safeNtlmString( String... strings )
+        {
+            for ( String string : strings )
+            {
+                if ( string != null )
+                {
+                    return string;
+                }
+            }
+            // avoid NPE from httpclient and trigger proper auth failure instead
+            return "";
+        }
+
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DemuxCredentialsProvider.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DemuxCredentialsProvider.java
new file mode 100644
index 0000000..f16246e
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/DemuxCredentialsProvider.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.CredentialsProvider;
+
+/**
+ * Credentials provider that helps to isolate server from proxy credentials. Apache HttpClient uses a single provider
+ * for both server and proxy auth, using the auth scope (host, port, etc.) to select the proper credentials. With regard
+ * to redirects, we use an auth scope for server credentials that's not specific enough to not be mistaken for proxy
+ * auth. This provider helps to maintain the proper isolation.
+ */
+final class DemuxCredentialsProvider
+    implements CredentialsProvider
+{
+
+    private final CredentialsProvider serverCredentialsProvider;
+
+    private final CredentialsProvider proxyCredentialsProvider;
+
+    private final HttpHost proxy;
+
+    public DemuxCredentialsProvider( CredentialsProvider serverCredentialsProvider,
+                                     CredentialsProvider proxyCredentialsProvider, HttpHost proxy )
+    {
+        this.serverCredentialsProvider = serverCredentialsProvider;
+        this.proxyCredentialsProvider = proxyCredentialsProvider;
+        this.proxy = proxy;
+    }
+
+    private CredentialsProvider getDelegate( AuthScope authScope )
+    {
+        if ( proxy.getPort() == authScope.getPort() && proxy.getHostName().equalsIgnoreCase( authScope.getHost() ) )
+        {
+            return proxyCredentialsProvider;
+        }
+        return serverCredentialsProvider;
+    }
+
+    public Credentials getCredentials( AuthScope authScope )
+    {
+        return getDelegate( authScope ).getCredentials( authScope );
+    }
+
+    public void setCredentials( AuthScope authScope, Credentials credentials )
+    {
+        getDelegate( authScope ).setCredentials( authScope, credentials );
+    }
+
+    public void clear()
+    {
+        serverCredentialsProvider.clear();
+        proxyCredentialsProvider.clear();
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/GlobalState.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/GlobalState.java
new file mode 100644
index 0000000..b3a9d4b
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/GlobalState.java
@@ -0,0 +1,215 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.http.HttpHost;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.conn.PoolingClientConnectionManager;
+import org.eclipse.aether.RepositoryCache;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * Container for HTTP-related state that can be shared across incarnations of the transporter to optimize the
+ * communication with servers.
+ */
+final class GlobalState
+    implements Closeable
+{
+
+    static class CompoundKey
+    {
+
+        private final Object[] keys;
+
+        public CompoundKey( Object... keys )
+        {
+            this.keys = keys;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            if ( obj == null || !getClass().equals( obj.getClass() ) )
+            {
+                return false;
+            }
+            CompoundKey that = (CompoundKey) obj;
+            return Arrays.equals( keys, that.keys );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            int hash = 17;
+            hash = hash * 31 + Arrays.hashCode( keys );
+            return hash;
+        }
+
+        @Override
+        public String toString()
+        {
+            return Arrays.toString( keys );
+        }
+    }
+
+    private static final String KEY = GlobalState.class.getName();
+
+    private static final String CONFIG_PROP_CACHE_STATE = "aether.connector.http.cacheState";
+
+    private final ConcurrentMap<SslConfig, ClientConnectionManager> connectionManagers;
+
+    private final ConcurrentMap<CompoundKey, Object> userTokens;
+
+    private final ConcurrentMap<HttpHost, AuthSchemePool> authSchemePools;
+
+    private final ConcurrentMap<CompoundKey, Boolean> expectContinues;
+
+    public static GlobalState get( RepositorySystemSession session )
+    {
+        GlobalState cache;
+        RepositoryCache repoCache = session.getCache();
+        if ( repoCache == null || !ConfigUtils.getBoolean( session, true, CONFIG_PROP_CACHE_STATE ) )
+        {
+            cache = null;
+        }
+        else
+        {
+            Object tmp = repoCache.get( session, KEY );
+            if ( tmp instanceof GlobalState )
+            {
+                cache = (GlobalState) tmp;
+            }
+            else
+            {
+                synchronized ( GlobalState.class )
+                {
+                    tmp = repoCache.get( session, KEY );
+                    if ( tmp instanceof GlobalState )
+                    {
+                        cache = (GlobalState) tmp;
+                    }
+                    else
+                    {
+                        cache = new GlobalState();
+                        repoCache.put( session, KEY, cache );
+                    }
+                }
+            }
+        }
+        return cache;
+    }
+
+    private GlobalState()
+    {
+        connectionManagers = new ConcurrentHashMap<SslConfig, ClientConnectionManager>();
+        userTokens = new ConcurrentHashMap<CompoundKey, Object>();
+        authSchemePools = new ConcurrentHashMap<HttpHost, AuthSchemePool>();
+        expectContinues = new ConcurrentHashMap<CompoundKey, Boolean>();
+    }
+
+    public void close()
+    {
+        for ( Iterator<Map.Entry<SslConfig, ClientConnectionManager>> it = connectionManagers.entrySet().iterator(); it.hasNext(); )
+        {
+            ClientConnectionManager connMgr = it.next().getValue();
+            it.remove();
+            connMgr.shutdown();
+        }
+    }
+
+    public ClientConnectionManager getConnectionManager( SslConfig config )
+    {
+        ClientConnectionManager manager = connectionManagers.get( config );
+        if ( manager == null )
+        {
+            ClientConnectionManager connMgr = newConnectionManager( config );
+            manager = connectionManagers.putIfAbsent( config, connMgr );
+            if ( manager != null )
+            {
+                connMgr.shutdown();
+            }
+            else
+            {
+                manager = connMgr;
+            }
+        }
+        return manager;
+    }
+
+    public static ClientConnectionManager newConnectionManager( SslConfig sslConfig )
+    {
+        SchemeRegistry schemeReg = new SchemeRegistry();
+        schemeReg.register( new Scheme( "http", 80, new PlainSocketFactory() ) );
+        schemeReg.register( new Scheme( "https", 443, new SslSocketFactory( sslConfig ) ) );
+
+        PoolingClientConnectionManager connMgr = new PoolingClientConnectionManager( schemeReg );
+        connMgr.setMaxTotal( 100 );
+        connMgr.setDefaultMaxPerRoute( 50 );
+        return connMgr;
+    }
+
+    public Object getUserToken( CompoundKey key )
+    {
+        return userTokens.get( key );
+    }
+
+    public void setUserToken( CompoundKey key, Object userToken )
+    {
+        if ( userToken != null )
+        {
+            userTokens.put( key, userToken );
+        }
+        else
+        {
+            userTokens.remove( key );
+        }
+    }
+
+    public ConcurrentMap<HttpHost, AuthSchemePool> getAuthSchemePools()
+    {
+        return authSchemePools;
+    }
+
+    public Boolean getExpectContinue( CompoundKey key )
+    {
+        return expectContinues.get( key );
+    }
+
+    public void setExpectContinue( CompoundKey key, boolean enabled )
+    {
+        expectContinues.put( key, enabled );
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpMkCol.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpMkCol.java
new file mode 100644
index 0000000..7a945ea
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpMkCol.java
@@ -0,0 +1,44 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+
+import org.apache.http.client.methods.HttpRequestBase;
+
+/**
+ * WebDAV MKCOL request to create parent directories.
+ */
+final class HttpMkCol
+    extends HttpRequestBase
+{
+
+    public HttpMkCol( URI uri )
+    {
+        setURI( uri );
+    }
+
+    @Override
+    public String getMethod()
+    {
+        return "MKCOL";
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java
new file mode 100644
index 0000000..de01a3d
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java
@@ -0,0 +1,598 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.params.AuthParams;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.DateUtils;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.conn.params.ConnRouteParams;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.DecompressingHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.TransportTask;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * A transporter for HTTP/HTTPS.
+ */
+final class HttpTransporter
+    extends AbstractTransporter
+{
+
+    private static final Pattern CONTENT_RANGE_PATTERN =
+        Pattern.compile( "\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*" );
+
+    private final Logger logger;
+
+    private final AuthenticationContext repoAuthContext;
+
+    private final AuthenticationContext proxyAuthContext;
+
+    private final URI baseUri;
+
+    private final HttpHost server;
+
+    private final HttpHost proxy;
+
+    private final HttpClient client;
+
+    private final Map<?, ?> headers;
+
+    private final LocalState state;
+
+    public HttpTransporter( RemoteRepository repository, RepositorySystemSession session, Logger logger )
+        throws NoTransporterException
+    {
+        if ( !"http".equalsIgnoreCase( repository.getProtocol() )
+            && !"https".equalsIgnoreCase( repository.getProtocol() ) )
+        {
+            throw new NoTransporterException( repository );
+        }
+        this.logger = logger;
+        try
+        {
+            baseUri = new URI( repository.getUrl() ).parseServerAuthority();
+            if ( baseUri.isOpaque() )
+            {
+                throw new URISyntaxException( repository.getUrl(), "URL must not be opaque" );
+            }
+            server = URIUtils.extractHost( baseUri );
+            if ( server == null )
+            {
+                throw new URISyntaxException( repository.getUrl(), "URL lacks host name" );
+            }
+        }
+        catch ( URISyntaxException e )
+        {
+            throw new NoTransporterException( repository, e.getMessage(), e );
+        }
+        proxy = toHost( repository.getProxy() );
+
+        repoAuthContext = AuthenticationContext.forRepository( session, repository );
+        proxyAuthContext = AuthenticationContext.forProxy( session, repository );
+
+        state = new LocalState( session, repository, new SslConfig( session, repoAuthContext ) );
+
+        headers =
+            ConfigUtils.getMap( session, Collections.emptyMap(), ConfigurationProperties.HTTP_HEADERS + "."
+                + repository.getId(), ConfigurationProperties.HTTP_HEADERS );
+
+        DefaultHttpClient client = new DefaultHttpClient( state.getConnectionManager() );
+
+        configureClient( client.getParams(), session, repository, proxy );
+
+        client.setCredentialsProvider( toCredentialsProvider( server, repoAuthContext, proxy, proxyAuthContext ) );
+
+        this.client = new DecompressingHttpClient( client );
+    }
+
+    private static HttpHost toHost( Proxy proxy )
+    {
+        HttpHost host = null;
+        if ( proxy != null )
+        {
+            host = new HttpHost( proxy.getHost(), proxy.getPort() );
+        }
+        return host;
+    }
+
+    private static void configureClient( HttpParams params, RepositorySystemSession session,
+                                         RemoteRepository repository, HttpHost proxy )
+    {
+        AuthParams.setCredentialCharset( params,
+                                         ConfigUtils.getString( session,
+                                                                ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
+                                                                ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "."
+                                                                    + repository.getId(),
+                                                                ConfigurationProperties.HTTP_CREDENTIAL_ENCODING ) );
+        ConnRouteParams.setDefaultProxy( params, proxy );
+        HttpConnectionParams.setConnectionTimeout( params,
+                                                   ConfigUtils.getInteger( session,
+                                                                           ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
+                                                                           ConfigurationProperties.CONNECT_TIMEOUT
+                                                                               + "." + repository.getId(),
+                                                                           ConfigurationProperties.CONNECT_TIMEOUT ) );
+        HttpConnectionParams.setSoTimeout( params,
+                                           ConfigUtils.getInteger( session,
+                                                                   ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
+                                                                   ConfigurationProperties.REQUEST_TIMEOUT + "."
+                                                                       + repository.getId(),
+                                                                   ConfigurationProperties.REQUEST_TIMEOUT ) );
+        HttpProtocolParams.setUserAgent( params, ConfigUtils.getString( session,
+                                                                        ConfigurationProperties.DEFAULT_USER_AGENT,
+                                                                        ConfigurationProperties.USER_AGENT ) );
+    }
+
+    private static CredentialsProvider toCredentialsProvider( HttpHost server, AuthenticationContext serverAuthCtx,
+                                                              HttpHost proxy, AuthenticationContext proxyAuthCtx )
+    {
+        CredentialsProvider provider = toCredentialsProvider( server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx );
+        if ( proxy != null )
+        {
+            CredentialsProvider p = toCredentialsProvider( proxy.getHostName(), proxy.getPort(), proxyAuthCtx );
+            provider = new DemuxCredentialsProvider( provider, p, proxy );
+        }
+        return provider;
+    }
+
+    private static CredentialsProvider toCredentialsProvider( String host, int port, AuthenticationContext ctx )
+    {
+        DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
+        if ( ctx != null )
+        {
+            AuthScope basicScope = new AuthScope( host, port );
+            provider.setCredentials( basicScope, new DeferredCredentialsProvider.BasicFactory( ctx ) );
+
+            AuthScope ntlmScope = new AuthScope( host, port, AuthScope.ANY_REALM, "ntlm" );
+            provider.setCredentials( ntlmScope, new DeferredCredentialsProvider.NtlmFactory( ctx ) );
+        }
+        return provider;
+    }
+
+    LocalState getState()
+    {
+        return state;
+    }
+
+    private URI resolve( TransportTask task )
+    {
+        return UriUtils.resolve( baseUri, task.getLocation() );
+    }
+
+    public int classify( Throwable error )
+    {
+        if ( error instanceof HttpResponseException
+            && ( (HttpResponseException) error ).getStatusCode() == HttpStatus.SC_NOT_FOUND )
+        {
+            return ERROR_NOT_FOUND;
+        }
+        return ERROR_OTHER;
+    }
+
+    @Override
+    protected void implPeek( PeekTask task )
+        throws Exception
+    {
+        HttpHead request = commonHeaders( new HttpHead( resolve( task ) ) );
+        execute( request, null );
+    }
+
+    @Override
+    protected void implGet( GetTask task )
+        throws Exception
+    {
+        EntityGetter getter = new EntityGetter( task );
+        HttpGet request = commonHeaders( new HttpGet( resolve( task ) ) );
+        resume( request, task );
+        try
+        {
+            execute( request, getter );
+        }
+        catch ( HttpResponseException e )
+        {
+            if ( e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED && request.containsHeader( HttpHeaders.RANGE ) )
+            {
+                request = commonHeaders( new HttpGet( request.getURI() ) );
+                execute( request, getter );
+                return;
+            }
+            throw e;
+        }
+    }
+
+    @Override
+    protected void implPut( PutTask task )
+        throws Exception
+    {
+        PutTaskEntity entity = new PutTaskEntity( task );
+        HttpPut request = commonHeaders( entity( new HttpPut( resolve( task ) ), entity ) );
+        try
+        {
+            execute( request, null );
+        }
+        catch ( HttpResponseException e )
+        {
+            if ( e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader( HttpHeaders.EXPECT ) )
+            {
+                state.setExpectContinue( false );
+                request = commonHeaders( entity( new HttpPut( request.getURI() ), entity ) );
+                execute( request, null );
+                return;
+            }
+            throw e;
+        }
+    }
+
+    private void execute( HttpUriRequest request, EntityGetter getter )
+        throws Exception
+    {
+        try
+        {
+            SharingHttpContext context = new SharingHttpContext( state );
+            prepare( request, context );
+            HttpResponse response = client.execute( server, request, context );
+            try
+            {
+                context.close();
+                handleStatus( response );
+                if ( getter != null )
+                {
+                    getter.handle( response );
+                }
+            }
+            finally
+            {
+                EntityUtils.consumeQuietly( response.getEntity() );
+            }
+        }
+        catch ( IOException e )
+        {
+            if ( e.getCause() instanceof TransferCancelledException )
+            {
+                throw (Exception) e.getCause();
+            }
+            throw e;
+        }
+    }
+
+    private void prepare( HttpUriRequest request, SharingHttpContext context )
+    {
+        boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase( request.getMethod() );
+        if ( state.getWebDav() == null && ( put || isPayloadPresent( request ) ) )
+        {
+            try
+            {
+                HttpOptions req = commonHeaders( new HttpOptions( request.getURI() ) );
+                HttpResponse response = client.execute( server, req, context );
+                state.setWebDav( isWebDav( response ) );
+                EntityUtils.consumeQuietly( response.getEntity() );
+            }
+            catch ( IOException e )
+            {
+                logger.debug( "Failed to prepare HTTP context", e );
+            }
+        }
+        if ( put && Boolean.TRUE.equals( state.getWebDav() ) )
+        {
+            mkdirs( request.getURI(), context );
+        }
+    }
+
+    private boolean isWebDav( HttpResponse response )
+    {
+        return response.containsHeader( HttpHeaders.DAV );
+    }
+
+    private void mkdirs( URI uri, SharingHttpContext context )
+    {
+        List<URI> dirs = UriUtils.getDirectories( baseUri, uri );
+        int index = 0;
+        for ( ; index < dirs.size(); index++ )
+        {
+            try
+            {
+                HttpResponse response =
+                    client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
+                try
+                {
+                    int status = response.getStatusLine().getStatusCode();
+                    if ( status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
+                    {
+                        break;
+                    }
+                    else if ( status == HttpStatus.SC_CONFLICT )
+                    {
+                        continue;
+                    }
+                    handleStatus( response );
+                }
+                finally
+                {
+                    EntityUtils.consumeQuietly( response.getEntity() );
+                }
+            }
+            catch ( IOException e )
+            {
+                logger.debug( "Failed to create parent directory " + dirs.get( index ), e );
+                return;
+            }
+        }
+        for ( index--; index >= 0; index-- )
+        {
+            try
+            {
+                HttpResponse response =
+                    client.execute( server, commonHeaders( new HttpMkCol( dirs.get( index ) ) ), context );
+                try
+                {
+                    handleStatus( response );
+                }
+                finally
+                {
+                    EntityUtils.consumeQuietly( response.getEntity() );
+                }
+            }
+            catch ( IOException e )
+            {
+                logger.debug( "Failed to create parent directory " + dirs.get( index ), e );
+                return;
+            }
+        }
+    }
+
+    private <T extends HttpEntityEnclosingRequest> T entity( T request, HttpEntity entity )
+    {
+        request.setEntity( entity );
+        return request;
+    }
+
+    private boolean isPayloadPresent( HttpUriRequest request )
+    {
+        if ( request instanceof HttpEntityEnclosingRequest )
+        {
+            HttpEntity entity = ( (HttpEntityEnclosingRequest) request ).getEntity();
+            return entity != null && entity.getContentLength() != 0;
+        }
+        return false;
+    }
+
+    private <T extends HttpUriRequest> T commonHeaders( T request )
+    {
+        request.setHeader( HttpHeaders.CACHE_CONTROL, "no-cache, no-store" );
+        request.setHeader( HttpHeaders.PRAGMA, "no-cache" );
+
+        if ( state.isExpectContinue() && isPayloadPresent( request ) )
+        {
+            request.setHeader( HttpHeaders.EXPECT, "100-continue" );
+        }
+
+        for ( Map.Entry<?, ?> entry : headers.entrySet() )
+        {
+            if ( !( entry.getKey() instanceof String ) )
+            {
+                continue;
+            }
+            if ( entry.getValue() instanceof String )
+            {
+                request.setHeader( entry.getKey().toString(), entry.getValue().toString() );
+            }
+            else
+            {
+                request.removeHeaders( entry.getKey().toString() );
+            }
+        }
+
+        if ( !state.isExpectContinue() )
+        {
+            request.removeHeaders( HttpHeaders.EXPECT );
+        }
+
+        return request;
+    }
+
+    private <T extends HttpUriRequest> T resume( T request, GetTask task )
+    {
+        long resumeOffset = task.getResumeOffset();
+        if ( resumeOffset > 0L && task.getDataFile() != null )
+        {
+            request.setHeader( HttpHeaders.RANGE, "bytes=" + Long.toString( resumeOffset ) + '-' );
+            request.setHeader( HttpHeaders.IF_UNMODIFIED_SINCE,
+                               DateUtils.formatDate( new Date( task.getDataFile().lastModified() - 60L * 1000L ) ) );
+            request.setHeader( HttpHeaders.ACCEPT_ENCODING, "identity" );
+        }
+        return request;
+    }
+
+    private void handleStatus( HttpResponse response )
+        throws HttpResponseException
+    {
+        int status = response.getStatusLine().getStatusCode();
+        if ( status >= 300 )
+        {
+            throw new HttpResponseException( status, response.getStatusLine().getReasonPhrase() + " (" + status + ")" );
+        }
+    }
+
+    @Override
+    protected void implClose()
+    {
+        AuthenticationContext.close( repoAuthContext );
+        AuthenticationContext.close( proxyAuthContext );
+        state.close();
+    }
+
+    private class EntityGetter
+    {
+
+        private final GetTask task;
+
+        public EntityGetter( GetTask task )
+        {
+            this.task = task;
+        }
+
+        public void handle( HttpResponse response )
+            throws IOException, TransferCancelledException
+        {
+            HttpEntity entity = response.getEntity();
+            if ( entity == null )
+            {
+                entity = new ByteArrayEntity( new byte[0] );
+            }
+
+            long offset = 0L, length = entity.getContentLength();
+            String range = getHeader( response, HttpHeaders.CONTENT_RANGE );
+            if ( range != null )
+            {
+                Matcher m = CONTENT_RANGE_PATTERN.matcher( range );
+                if ( !m.matches() )
+                {
+                    throw new IOException( "Invalid Content-Range header for partial download: " + range );
+                }
+                offset = Long.parseLong( m.group( 1 ) );
+                length = Long.parseLong( m.group( 2 ) ) + 1L;
+                if ( offset < 0L || offset >= length || ( offset > 0L && offset != task.getResumeOffset() ) )
+                {
+                    throw new IOException( "Invalid Content-Range header for partial download from offset "
+                        + task.getResumeOffset() + ": " + range );
+                }
+            }
+
+            InputStream is = entity.getContent();
+            utilGet( task, is, true, length, offset > 0L );
+            extractChecksums( response );
+        }
+
+        private void extractChecksums( HttpResponse response )
+        {
+            // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}"
+            String etag = getHeader( response, HttpHeaders.ETAG );
+            if ( etag != null )
+            {
+                int start = etag.indexOf( "SHA1{" ), end = etag.indexOf( "}", start + 5 );
+                if ( start >= 0 && end > start )
+                {
+                    task.setChecksum( "SHA-1", etag.substring( start + 5, end ) );
+                }
+            }
+        }
+
+        private String getHeader( HttpResponse response, String name )
+        {
+            Header header = response.getFirstHeader( name );
+            return ( header != null ) ? header.getValue() : null;
+        }
+
+    }
+
+    private class PutTaskEntity
+        extends AbstractHttpEntity
+    {
+
+        private final PutTask task;
+
+        public PutTaskEntity( PutTask task )
+        {
+            this.task = task;
+        }
+
+        public boolean isRepeatable()
+        {
+            return true;
+        }
+
+        public boolean isStreaming()
+        {
+            return false;
+        }
+
+        public long getContentLength()
+        {
+            return task.getDataLength();
+        }
+
+        public InputStream getContent()
+            throws IOException
+        {
+            return task.newInputStream();
+        }
+
+        public void writeTo( OutputStream os )
+            throws IOException
+        {
+            try
+            {
+                utilPut( task, os, false );
+            }
+            catch ( TransferCancelledException e )
+            {
+                throw (IOException) new InterruptedIOException().initCause( e );
+            }
+        }
+
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporterFactory.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporterFactory.java
new file mode 100644
index 0000000..77d2141
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporterFactory.java
@@ -0,0 +1,105 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A transporter factory for repositories using the {@code http:} or {@code https:} protocol. The provided transporters
+ * support uploads to WebDAV servers and resumable downloads.
+ */
+@Named( "http" )
+public final class HttpTransporterFactory
+    implements TransporterFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private float priority = 5.0f;
+
+    /**
+     * Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation
+     * by clients, the new factory needs to be configured via its various mutators before first use or runtime errors
+     * will occur.
+     */
+    public HttpTransporterFactory()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    HttpTransporterFactory( LoggerFactory loggerFactory )
+    {
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+    }
+
+    /**
+     * Sets the logger factory to use for this component.
+     * 
+     * @param loggerFactory The logger factory to use, may be {@code null} to disable logging.
+     * @return This component for chaining, never {@code null}.
+     */
+    public HttpTransporterFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, HttpTransporter.class );
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public HttpTransporterFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException
+    {
+        return new HttpTransporter( repository, session, logger );
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/LocalState.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/LocalState.java
new file mode 100644
index 0000000..cbf2d93
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/LocalState.java
@@ -0,0 +1,162 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScheme;
+import org.apache.http.conn.ClientConnectionManager;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.transport.http.GlobalState.CompoundKey;
+
+/**
+ * Container for HTTP-related state that can be shared across invocations of the transporter to optimize the
+ * communication with server.
+ */
+final class LocalState
+    implements Closeable
+{
+
+    private final GlobalState global;
+
+    private final ClientConnectionManager connMgr;
+
+    private final CompoundKey userTokenKey;
+
+    private volatile Object userToken;
+
+    private final CompoundKey expectContinueKey;
+
+    private volatile Boolean expectContinue;
+
+    private volatile Boolean webDav;
+
+    private final ConcurrentMap<HttpHost, AuthSchemePool> authSchemePools;
+
+    public LocalState( RepositorySystemSession session, RemoteRepository repo, SslConfig sslConfig )
+    {
+        global = GlobalState.get( session );
+        userToken = this;
+        if ( global == null )
+        {
+            connMgr = GlobalState.newConnectionManager( sslConfig );
+            userTokenKey = null;
+            expectContinueKey = null;
+            authSchemePools = new ConcurrentHashMap<HttpHost, AuthSchemePool>();
+        }
+        else
+        {
+            connMgr = global.getConnectionManager( sslConfig );
+            userTokenKey = new CompoundKey( repo.getId(), repo.getUrl(), repo.getAuthentication(), repo.getProxy() );
+            expectContinueKey = new CompoundKey( repo.getUrl(), repo.getProxy() );
+            authSchemePools = global.getAuthSchemePools();
+        }
+    }
+
+    public ClientConnectionManager getConnectionManager()
+    {
+        return connMgr;
+    }
+
+    public Object getUserToken()
+    {
+        if ( userToken == this )
+        {
+            userToken = ( global != null ) ? global.getUserToken( userTokenKey ) : null;
+        }
+        return userToken;
+    }
+
+    public void setUserToken( Object userToken )
+    {
+        this.userToken = userToken;
+        if ( global != null )
+        {
+            global.setUserToken( userTokenKey, userToken );
+        }
+    }
+
+    public boolean isExpectContinue()
+    {
+        if ( expectContinue == null )
+        {
+            expectContinue =
+                !Boolean.FALSE.equals( ( global != null ) ? global.getExpectContinue( expectContinueKey ) : null );
+        }
+        return expectContinue;
+    }
+
+    public void setExpectContinue( boolean enabled )
+    {
+        expectContinue = enabled;
+        if ( global != null )
+        {
+            global.setExpectContinue( expectContinueKey, enabled );
+        }
+    }
+
+    public Boolean getWebDav()
+    {
+        return webDav;
+    }
+
+    public void setWebDav( boolean webDav )
+    {
+        this.webDav = webDav;
+    }
+
+    public AuthScheme getAuthScheme( HttpHost host )
+    {
+        AuthSchemePool pool = authSchemePools.get( host );
+        if ( pool != null )
+        {
+            return pool.get();
+        }
+        return null;
+    }
+
+    public void setAuthScheme( HttpHost host, AuthScheme authScheme )
+    {
+        AuthSchemePool pool = authSchemePools.get( host );
+        if ( pool == null )
+        {
+            AuthSchemePool p = new AuthSchemePool();
+            pool = authSchemePools.putIfAbsent( host, p );
+            if ( pool == null )
+            {
+                pool = p;
+            }
+        }
+        pool.put( authScheme );
+    }
+
+    public void close()
+    {
+        if ( global == null )
+        {
+            connMgr.shutdown();
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingAuthCache.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingAuthCache.java
new file mode 100644
index 0000000..1264d04
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingAuthCache.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScheme;
+import org.apache.http.client.AuthCache;
+
+/**
+ * Auth scheme cache that upon clearing releases all cached schemes into a pool for future reuse by other requests,
+ * thereby reducing challenge-response roundtrips.
+ */
+final class SharingAuthCache
+    implements AuthCache
+{
+
+    private final LocalState state;
+
+    private final Map<HttpHost, AuthScheme> authSchemes;
+
+    public SharingAuthCache( LocalState state )
+    {
+        this.state = state;
+        authSchemes = new HashMap<HttpHost, AuthScheme>();
+    }
+
+    private static HttpHost toKey( HttpHost host )
+    {
+        if ( host.getPort() <= 0 )
+        {
+            int port = host.getSchemeName().equalsIgnoreCase( "https" ) ? 443 : 80;
+            return new HttpHost( host.getHostName(), port, host.getSchemeName() );
+        }
+        return host;
+    }
+
+    public AuthScheme get( HttpHost host )
+    {
+        host = toKey( host );
+        AuthScheme authScheme = authSchemes.get( host );
+        if ( authScheme == null )
+        {
+            authScheme = state.getAuthScheme( host );
+            authSchemes.put( host, authScheme );
+        }
+        return authScheme;
+    }
+
+    public void put( HttpHost host, AuthScheme authScheme )
+    {
+        if ( authScheme != null )
+        {
+            authSchemes.put( toKey( host ), authScheme );
+        }
+        else
+        {
+            remove( host );
+        }
+    }
+
+    public void remove( HttpHost host )
+    {
+        authSchemes.remove( toKey( host ) );
+    }
+
+    public void clear()
+    {
+        share();
+        authSchemes.clear();
+    }
+
+    private void share()
+    {
+        for ( Map.Entry<HttpHost, AuthScheme> entry : authSchemes.entrySet() )
+        {
+            state.setAuthScheme( entry.getKey(), entry.getValue() );
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return authSchemes.toString();
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingHttpContext.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingHttpContext.java
new file mode 100644
index 0000000..4464c26
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SharingHttpContext.java
@@ -0,0 +1,88 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.Closeable;
+
+import org.apache.http.client.protocol.ClientContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+/**
+ * HTTP context that shares certain attributes among requests to optimize the communication with the server.
+ * 
+ * @see <a href="http://hc.apache.org/httpcomponents-client-ga/tutorial/html/advanced.html#stateful_conn">Stateful HTTP
+ *      connections</a>
+ */
+final class SharingHttpContext
+    extends BasicHttpContext
+    implements Closeable
+{
+
+    private final LocalState state;
+
+    private final SharingAuthCache authCache;
+
+    public SharingHttpContext( LocalState state )
+    {
+        this.state = state;
+        authCache = new SharingAuthCache( state );
+        super.setAttribute( ClientContext.AUTH_CACHE, authCache );
+    }
+
+    @Override
+    public Object getAttribute( String id )
+    {
+        if ( ClientContext.USER_TOKEN.equals( id ) )
+        {
+            return state.getUserToken();
+        }
+        return super.getAttribute( id );
+    }
+
+    @Override
+    public void setAttribute( String id, Object obj )
+    {
+        if ( ClientContext.USER_TOKEN.equals( id ) )
+        {
+            state.setUserToken( obj );
+        }
+        else
+        {
+            super.setAttribute( id, obj );
+        }
+    }
+
+    @Override
+    public Object removeAttribute( String id )
+    {
+        if ( ClientContext.USER_TOKEN.equals( id ) )
+        {
+            state.setUserToken( null );
+            return null;
+        }
+        return super.removeAttribute( id );
+    }
+
+    public void close()
+    {
+        authCache.clear();
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslConfig.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslConfig.java
new file mode 100644
index 0000000..d991796
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslConfig.java
@@ -0,0 +1,117 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * SSL-related configuration and cache key for connection pools (whose scheme registries are derived from this config).
+ */
+final class SslConfig
+{
+
+    private static final String CIPHER_SUITES = "https.cipherSuites";
+
+    private static final String PROTOCOLS = "https.protocols";
+
+    final SSLContext context;
+
+    final HostnameVerifier verifier;
+
+    final String[] cipherSuites;
+
+    final String[] protocols;
+
+    public SslConfig( RepositorySystemSession session, AuthenticationContext authContext )
+    {
+        context =
+            ( authContext != null ) ? authContext.get( AuthenticationContext.SSL_CONTEXT, SSLContext.class ) : null;
+        verifier =
+            ( authContext != null ) ? authContext.get( AuthenticationContext.SSL_HOSTNAME_VERIFIER,
+                                                       HostnameVerifier.class ) : null;
+
+        cipherSuites = split( get( session, CIPHER_SUITES ) );
+        protocols = split( get( session, PROTOCOLS ) );
+    }
+
+    private static String get( RepositorySystemSession session, String key )
+    {
+        String value = ConfigUtils.getString( session, null, "aether.connector." + key, key );
+        if ( value == null )
+        {
+            value = System.getProperty( key );
+        }
+        return value;
+    }
+
+    private static String[] split( String value )
+    {
+        if ( value == null || value.length() <= 0 )
+        {
+            return null;
+        }
+        return value.split( ",+" );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        SslConfig that = (SslConfig) obj;
+        return eq( context, that.context ) && eq( verifier, that.verifier )
+            && Arrays.equals( cipherSuites, that.cipherSuites ) && Arrays.equals( protocols, that.protocols );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( context );
+        hash = hash * 31 + hash( verifier );
+        hash = hash * 31 + Arrays.hashCode( cipherSuites );
+        hash = hash * 31 + Arrays.hashCode( protocols );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslSocketFactory.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslSocketFactory.java
new file mode 100644
index 0000000..5189c87
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/SslSocketFactory.java
@@ -0,0 +1,87 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+
+/**
+ * Specialized SSL socket factory to more closely resemble the JRE's HttpsClient and respect well-known SSL-related
+ * configuration properties.
+ * 
+ * @see <a href="http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#Customization">JSSE
+ *      Reference Guide, Customization</a>
+ */
+final class SslSocketFactory
+    extends org.apache.http.conn.ssl.SSLSocketFactory
+{
+
+    private final String[] cipherSuites;
+
+    private final String[] protocols;
+
+    public SslSocketFactory( SslConfig config )
+    {
+        this( getSocketFactory( config.context ), getHostnameVerifier( config.verifier ), config.cipherSuites,
+              config.protocols );
+    }
+
+    private static SSLSocketFactory getSocketFactory( SSLContext context )
+    {
+        return ( context != null ) ? context.getSocketFactory() : (SSLSocketFactory) SSLSocketFactory.getDefault();
+    }
+
+    private static X509HostnameVerifier getHostnameVerifier( HostnameVerifier verifier )
+    {
+        return ( verifier != null ) ? X509HostnameVerifierAdapter.adapt( verifier )
+                        : org.apache.http.conn.ssl.SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+    }
+
+    private SslSocketFactory( SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier,
+                              String[] cipherSuites, String[] protocols )
+    {
+        super( socketfactory, hostnameVerifier );
+
+        this.cipherSuites = cipherSuites;
+        this.protocols = protocols;
+    }
+
+    @Override
+    protected void prepareSocket( SSLSocket socket )
+        throws IOException
+    {
+        super.prepareSocket( socket );
+        if ( cipherSuites != null )
+        {
+            socket.setEnabledCipherSuites( cipherSuites );
+        }
+        if ( protocols != null )
+        {
+            socket.setEnabledProtocols( protocols );
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/UriUtils.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/UriUtils.java
new file mode 100644
index 0000000..7bc19da
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/UriUtils.java
@@ -0,0 +1,84 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.client.utils.URIUtils;
+
+/**
+ * Helps to deal with URIs.
+ */
+final class UriUtils
+{
+
+    public static URI resolve( URI base, URI ref )
+    {
+        String path = ref.getRawPath();
+        if ( path != null && path.length() > 0 )
+        {
+            path = base.getRawPath();
+            if ( path == null || !path.endsWith( "/" ) )
+            {
+                try
+                {
+                    base = new URI( base.getScheme(), base.getAuthority(), base.getPath() + '/', null, null );
+                }
+                catch ( URISyntaxException e )
+                {
+                    throw new IllegalStateException( e );
+                }
+            }
+        }
+        return URIUtils.resolve( base, ref );
+    }
+
+    public static List<URI> getDirectories( URI base, URI uri )
+    {
+        List<URI> dirs = new ArrayList<URI>();
+        for ( URI dir = uri.resolve( "." ); !isBase( base, dir ); dir = dir.resolve( ".." ) )
+        {
+            dirs.add( dir );
+        }
+        return dirs;
+    }
+
+    private static boolean isBase( URI base, URI uri )
+    {
+        String path = uri.getRawPath();
+        if ( path == null || "/".equals( path ) )
+        {
+            return true;
+        }
+        if ( base != null )
+        {
+            URI rel = base.relativize( uri );
+            if ( rel.getRawPath() == null || rel.getRawPath().length() <= 0 || rel.equals( uri ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/X509HostnameVerifierAdapter.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/X509HostnameVerifierAdapter.java
new file mode 100644
index 0000000..007f660
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/X509HostnameVerifierAdapter.java
@@ -0,0 +1,81 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+
+/**
+ * Makes a standard hostname verifier compatible with Apache HttpClient's API.
+ */
+final class X509HostnameVerifierAdapter
+    implements X509HostnameVerifier
+{
+
+    private final HostnameVerifier verifier;
+
+    public static X509HostnameVerifier adapt( HostnameVerifier verifier )
+    {
+        if ( verifier instanceof X509HostnameVerifier )
+        {
+            return (X509HostnameVerifier) verifier;
+        }
+        return new X509HostnameVerifierAdapter( verifier );
+    }
+
+    private X509HostnameVerifierAdapter( HostnameVerifier verifier )
+    {
+        this.verifier = verifier;
+    }
+
+    public boolean verify( String hostname, SSLSession session )
+    {
+        return verifier.verify( hostname, session );
+    }
+
+    public void verify( String host, SSLSocket socket )
+        throws IOException
+    {
+        if ( !verify( host, socket.getSession() ) )
+        {
+            throw new SSLException( "<" + host + "> does not pass hostname verification" );
+        }
+    }
+
+    public void verify( String host, X509Certificate cert )
+        throws SSLException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void verify( String host, String[] cns, String[] subjectAlts )
+        throws SSLException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/package-info.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/package-info.java
new file mode 100644
index 0000000..828299e
--- /dev/null
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/package-info.java
@@ -0,0 +1,25 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Support for downloads/uploads via the HTTP and HTTPS protocols. The current implementation is backed by
+ * <a href="http://hc.apache.org/httpcomponents-client-ga/" target="_blank">Apache HttpClient</a>.
+ */
+package org.eclipse.aether.transport.http;
+
diff --git a/maven-resolver-transport-http/src/site/site.xml b/maven-resolver-transport-http/src/site/site.xml
new file mode 100644
index 0000000..2e6e8ba
--- /dev/null
+++ b/maven-resolver-transport-http/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Transport HTTP">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java
new file mode 100644
index 0000000..ae6980d
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpServer.java
@@ -0,0 +1,586 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.aether.util.ChecksumUtils;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.util.B64Code;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HttpServer
+{
+
+    public static class LogEntry
+    {
+
+        public final String method;
+
+        public final String path;
+
+        public final Map<String, String> headers;
+
+        public LogEntry( String method, String path, Map<String, String> headers )
+        {
+            this.method = method;
+            this.path = path;
+            this.headers = headers;
+        }
+
+        @Override
+        public String toString()
+        {
+            return method + " " + path;
+        }
+
+    }
+
+    public enum ExpectContinue
+    {
+        FAIL, PROPER, BROKEN
+    }
+
+    public enum ChecksumHeader
+    {
+        NEXUS
+    }
+
+    private static final Logger log = LoggerFactory.getLogger( HttpServer.class );
+
+    private File repoDir;
+
+    private boolean rangeSupport = true;
+
+    private boolean webDav;
+
+    private ExpectContinue expectContinue = ExpectContinue.PROPER;
+
+    private ChecksumHeader checksumHeader;
+
+    private Server server;
+
+    private ServerConnector httpConnector;
+
+    private ServerConnector httpsConnector;
+
+    private String username;
+
+    private String password;
+
+    private String proxyUsername;
+
+    private String proxyPassword;
+
+    private List<LogEntry> logEntries = Collections.synchronizedList( new ArrayList<LogEntry>() );
+
+    public String getHost()
+    {
+        return "localhost";
+    }
+
+    public int getHttpPort()
+    {
+        return httpConnector != null ? httpConnector.getLocalPort() : -1;
+    }
+
+    public int getHttpsPort()
+    {
+        return httpsConnector != null ? httpsConnector.getLocalPort() : -1;
+    }
+
+    public String getHttpUrl()
+    {
+        return "http://" + getHost() + ":" + getHttpPort();
+    }
+
+    public String getHttpsUrl()
+    {
+        return "https://" + getHost() + ":" + getHttpsPort();
+    }
+
+    public HttpServer addSslConnector()
+    {
+        if ( httpsConnector == null )
+        {
+            SslContextFactory ssl = new SslContextFactory();
+            ssl.setKeyStorePath( new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
+            ssl.setKeyStorePassword( "server-pwd" );
+            ssl.setTrustStorePath( new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
+            ssl.setTrustStorePassword( "client-pwd" );
+            ssl.setNeedClientAuth( true );
+            httpsConnector = new ServerConnector( server, ssl );
+            server.addConnector( httpsConnector );
+            try
+            {
+                httpsConnector.start();
+            }
+            catch ( Exception e )
+            {
+                throw new IllegalStateException( e );
+            }
+        }
+        return this;
+    }
+
+    public List<LogEntry> getLogEntries()
+    {
+        return logEntries;
+    }
+
+    public HttpServer setRepoDir( File repoDir )
+    {
+        this.repoDir = repoDir;
+        return this;
+    }
+
+    public HttpServer setRangeSupport( boolean rangeSupport )
+    {
+        this.rangeSupport = rangeSupport;
+        return this;
+    }
+
+    public HttpServer setWebDav( boolean webDav )
+    {
+        this.webDav = webDav;
+        return this;
+    }
+
+    public HttpServer setExpectSupport( ExpectContinue expectContinue )
+    {
+        this.expectContinue = expectContinue;
+        return this;
+    }
+
+    public HttpServer setChecksumHeader( ChecksumHeader checksumHeader )
+    {
+        this.checksumHeader = checksumHeader;
+        return this;
+    }
+
+    public HttpServer setAuthentication( String username, String password )
+    {
+        this.username = username;
+        this.password = password;
+        return this;
+    }
+
+    public HttpServer setProxyAuthentication( String username, String password )
+    {
+        proxyUsername = username;
+        proxyPassword = password;
+        return this;
+    }
+
+    public HttpServer start()
+        throws Exception
+    {
+        if ( server != null )
+        {
+            return this;
+        }
+
+        HandlerList handlers = new HandlerList();
+        handlers.addHandler( new LogHandler() );
+        handlers.addHandler( new ProxyAuthHandler() );
+        handlers.addHandler( new AuthHandler() );
+        handlers.addHandler( new RedirectHandler() );
+        handlers.addHandler( new RepoHandler() );
+
+        server = new Server();
+        httpConnector = new ServerConnector( server );
+        server.addConnector( httpConnector );
+        server.setHandler( handlers );
+        server.start();
+
+        return this;
+    }
+
+    public void stop()
+        throws Exception
+    {
+        if ( server != null )
+        {
+            server.stop();
+            server = null;
+            httpConnector = null;
+            httpsConnector = null;
+        }
+    }
+
+    private class LogHandler
+        extends AbstractHandler
+    {
+
+        @SuppressWarnings( "unchecked" )
+        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
+            throws IOException
+        {
+            log.info( "{} {}{}", new Object[] { req.getMethod(), req.getRequestURL(),
+                req.getQueryString() != null ? "?" + req.getQueryString() : "" } );
+
+            Map<String, String> headers = new TreeMap<String, String>( String.CASE_INSENSITIVE_ORDER );
+            for ( Enumeration<String> en = req.getHeaderNames(); en.hasMoreElements(); )
+            {
+                String name = en.nextElement();
+                StringBuilder buffer = new StringBuilder( 128 );
+                for ( Enumeration<String> ien = req.getHeaders( name ); ien.hasMoreElements(); )
+                {
+                    if ( buffer.length() > 0 )
+                    {
+                        buffer.append( ", " );
+                    }
+                    buffer.append( ien.nextElement() );
+                }
+                headers.put( name, buffer.toString() );
+            }
+            logEntries.add( new LogEntry( req.getMethod(), req.getPathInfo(), Collections.unmodifiableMap( headers ) ) );
+        }
+
+    }
+
+    private class RepoHandler
+        extends AbstractHandler
+    {
+
+        private final Pattern SIMPLE_RANGE = Pattern.compile( "bytes=([0-9])+-" );
+
+        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
+            throws IOException
+        {
+            String path = req.getPathInfo().substring( 1 );
+
+            if ( !path.startsWith( "repo/" ) )
+            {
+                return;
+            }
+            req.setHandled( true );
+
+            if ( ExpectContinue.FAIL.equals( expectContinue ) && request.getHeader( HttpHeader.EXPECT.asString() ) != null )
+            {
+                response.setStatus( HttpServletResponse.SC_EXPECTATION_FAILED );
+                return;
+            }
+
+            File file = new File( repoDir, path.substring( 5 ) );
+            if ( HttpMethod.GET.is( req.getMethod() ) || HttpMethod.HEAD.is( req.getMethod() ) )
+            {
+                if ( !file.isFile() || path.endsWith( URIUtil.SLASH ) )
+                {
+                    response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+                    return;
+                }
+                long ifUnmodifiedSince = request.getDateHeader( HttpHeader.IF_UNMODIFIED_SINCE.asString() );
+                if ( ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince )
+                {
+                    response.setStatus( HttpServletResponse.SC_PRECONDITION_FAILED );
+                    return;
+                }
+                long offset = 0L;
+                String range = request.getHeader( HttpHeader.RANGE.asString() );
+                if ( range != null && rangeSupport )
+                {
+                    Matcher m = SIMPLE_RANGE.matcher( range );
+                    if ( m.matches() )
+                    {
+                        offset = Long.parseLong( m.group( 1 ) );
+                        if ( offset >= file.length() )
+                        {
+                            response.setStatus( HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE );
+                            return;
+                        }
+                    }
+                    String encoding = request.getHeader( HttpHeader.ACCEPT_ENCODING.asString() );
+                    if ( ( encoding != null && !"identity".equals( encoding ) ) || ifUnmodifiedSince == -1L )
+                    {
+                        response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+                        return;
+                    }
+                }
+                response.setStatus( ( offset > 0L ) ? HttpServletResponse.SC_PARTIAL_CONTENT : HttpServletResponse.SC_OK );
+                response.setDateHeader( HttpHeader.LAST_MODIFIED.asString(), file.lastModified() );
+                response.setHeader( HttpHeader.CONTENT_LENGTH.asString(), Long.toString( file.length() - offset ) );
+                if ( offset > 0L )
+                {
+                    response.setHeader( HttpHeader.CONTENT_RANGE.asString(), "bytes " + offset + "-" + ( file.length() - 1L )
+                        + "/" + file.length() );
+                }
+                if ( checksumHeader != null )
+                {
+                    Map<String, Object> checksums = ChecksumUtils.calc( file, Collections.singleton( "SHA-1" ) );
+                    switch ( checksumHeader )
+                    {
+                        case NEXUS:
+                            response.setHeader( HttpHeader.ETAG.asString(), "{SHA1{" + checksums.get( "SHA-1" ) + "}}" );
+                            break;
+                    }
+                }
+                if ( HttpMethod.HEAD.is( req.getMethod() ) )
+                {
+                    return;
+                }
+                FileInputStream is = null;
+                try
+                {
+                    is = new FileInputStream( file );
+                    if ( offset > 0L )
+                    {
+                        long skipped = is.skip( offset );
+                        while ( skipped < offset && is.read() >= 0 )
+                        {
+                            skipped++;
+                        }
+                    }
+                    IO.copy( is, response.getOutputStream() );
+                    is.close();
+                    is = null;
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( is != null )
+                        {
+                            is.close();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                }
+            }
+            else if ( HttpMethod.PUT.is( req.getMethod() ) )
+            {
+                if ( !webDav )
+                {
+                    file.getParentFile().mkdirs();
+                }
+                if ( file.getParentFile().exists() )
+                {
+                    try
+                    {
+                        FileOutputStream os = null;
+                        try
+                        {
+                            os = new FileOutputStream( file );
+                            IO.copy( request.getInputStream(), os );
+                            os.close();
+                            os = null;
+                        }
+                        finally
+                        {
+                            try
+                            {
+                                if ( os != null )
+                                {
+                                    os.close();
+                                }
+                            }
+                            catch ( final IOException e )
+                            {
+                                // Suppressed due to an exception already thrown in the try block.
+                            }
+                        }
+                    }
+                    catch ( IOException e )
+                    {
+                        file.delete();
+                        throw e;
+                    }
+                    response.setStatus( HttpServletResponse.SC_NO_CONTENT );
+                }
+                else
+                {
+                    response.setStatus( HttpServletResponse.SC_FORBIDDEN );
+                }
+            }
+            else if ( HttpMethod.OPTIONS.is( req.getMethod() ) )
+            {
+                if ( webDav )
+                {
+                    response.setHeader( "DAV", "1,2" );
+                }
+                response.setHeader( HttpHeader.ALLOW.asString(), "GET, PUT, HEAD, OPTIONS" );
+                response.setStatus( HttpServletResponse.SC_OK );
+            }
+            else if ( webDav && "MKCOL".equals( req.getMethod() ) )
+            {
+                if ( file.exists() )
+                {
+                    response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
+                }
+                else if ( file.mkdir() )
+                {
+                    response.setStatus( HttpServletResponse.SC_CREATED );
+                }
+                else
+                {
+                    response.setStatus( HttpServletResponse.SC_CONFLICT );
+                }
+            }
+            else
+            {
+                response.setStatus( HttpServletResponse.SC_METHOD_NOT_ALLOWED );
+            }
+        }
+
+    }
+
+    private class RedirectHandler
+        extends AbstractHandler
+    {
+
+        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
+            throws IOException
+        {
+            String path = req.getPathInfo();
+            if ( !path.startsWith( "/redirect/" ) )
+            {
+                return;
+            }
+            req.setHandled( true );
+            StringBuilder location = new StringBuilder( 128 );
+            String scheme = req.getParameter( "scheme" );
+            location.append( scheme != null ? scheme : req.getScheme() );
+            location.append( "://" );
+            location.append( req.getServerName() );
+            location.append( ":" );
+            if ( "http".equalsIgnoreCase( scheme ) )
+            {
+                location.append( getHttpPort() );
+            }
+            else if ( "https".equalsIgnoreCase( scheme ) )
+            {
+                location.append( getHttpsPort() );
+            }
+            else
+            {
+                location.append( req.getServerPort() );
+            }
+            location.append( "/repo" ).append( path.substring( 9 ) );
+            response.setStatus( HttpServletResponse.SC_MOVED_PERMANENTLY );
+            response.setHeader( HttpHeader.LOCATION.asString(), location.toString() );
+        }
+
+    }
+
+    private class AuthHandler
+        extends AbstractHandler
+    {
+
+        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
+            throws IOException
+        {
+            if ( ExpectContinue.BROKEN.equals( expectContinue )
+                && "100-continue".equalsIgnoreCase( request.getHeader( HttpHeader.EXPECT.asString() ) ) )
+            {
+                request.getInputStream();
+            }
+
+            if ( username != null && password != null )
+            {
+                if ( checkBasicAuth( request.getHeader( HttpHeader.AUTHORIZATION.asString() ), username, password ) )
+                {
+                    return;
+                }
+                req.setHandled( true );
+                response.setHeader( HttpHeader.WWW_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
+                response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
+            }
+        }
+
+    }
+
+    private class ProxyAuthHandler
+        extends AbstractHandler
+    {
+
+        public void handle( String target, Request req, HttpServletRequest request, HttpServletResponse response )
+            throws IOException
+        {
+            if ( proxyUsername != null && proxyPassword != null )
+            {
+                if ( checkBasicAuth( request.getHeader( HttpHeader.PROXY_AUTHORIZATION.asString() ), proxyUsername, proxyPassword ) )
+                {
+                    return;
+                }
+                req.setHandled( true );
+                response.setHeader( HttpHeader.PROXY_AUTHENTICATE.asString(), "basic realm=\"Test-Realm\"" );
+                response.setStatus( HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED );
+            }
+        }
+
+    }
+
+    static boolean checkBasicAuth( String credentials, String username, String password )
+    {
+        if ( credentials != null )
+        {
+            int space = credentials.indexOf( ' ' );
+            if ( space > 0 )
+            {
+                String method = credentials.substring( 0, space );
+                if ( "basic".equalsIgnoreCase( method ) )
+                {
+                    credentials = credentials.substring( space + 1 );
+                    credentials = B64Code.decode( credentials, StringUtil.__ISO_8859_1 );
+                    int i = credentials.indexOf( ':' );
+                    if ( i > 0 )
+                    {
+                        String user = credentials.substring( 0, i );
+                        String pass = credentials.substring( i + 1 );
+                        if ( username.equals( user ) && password.equals( pass ) )
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java
new file mode 100644
index 0000000..1b03aaa
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java
@@ -0,0 +1,1163 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.ConnectException;
+import java.net.ServerSocket;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.pool.ConnPoolControl;
+import org.apache.http.pool.PoolStats;
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.DefaultRepositoryCache;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+/**
+ */
+public class HttpTransporterTest
+{
+
+    static
+    {
+        System.setProperty( "javax.net.ssl.trustStore",
+                            new File( "src/test/resources/ssl/server-store" ).getAbsolutePath() );
+        System.setProperty( "javax.net.ssl.trustStorePassword", "server-pwd" );
+        System.setProperty( "javax.net.ssl.keyStore",
+                            new File( "src/test/resources/ssl/client-store" ).getAbsolutePath() );
+        System.setProperty( "javax.net.ssl.keyStorePassword", "client-pwd" );
+    }
+
+    @Rule
+    public TestName testName = new TestName();
+
+    private DefaultRepositorySystemSession session;
+
+    private TransporterFactory factory;
+
+    private Transporter transporter;
+
+    private File repoDir;
+
+    private HttpServer httpServer;
+
+    private Authentication auth;
+
+    private Proxy proxy;
+
+    private RemoteRepository newRepo( String url )
+    {
+        return new RemoteRepository.Builder( "test", "default", url ).setAuthentication( auth ).setProxy( proxy ).build();
+    }
+
+    private void newTransporter( String url )
+        throws Exception
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        transporter = factory.newInstance( session, newRepo( url ) );
+    }
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        System.out.println( "=== " + testName.getMethodName() + " ===" );
+        session = TestUtils.newSession();
+        factory = new HttpTransporterFactory( new TestLoggerFactory() );
+        repoDir = TestFileUtils.createTempDir();
+        TestFileUtils.writeString( new File( repoDir, "file.txt" ), "test" );
+        TestFileUtils.writeString( new File( repoDir, "dir/file.txt" ), "test" );
+        TestFileUtils.writeString( new File( repoDir, "empty.txt" ), "" );
+        TestFileUtils.writeString( new File( repoDir, "some space.txt" ), "space" );
+        File resumable = new File( repoDir, "resume.txt" );
+        TestFileUtils.writeString( resumable, "resumable" );
+        resumable.setLastModified( System.currentTimeMillis() - 90 * 1000 );
+        httpServer = new HttpServer().setRepoDir( repoDir ).start();
+        newTransporter( httpServer.getHttpUrl() );
+    }
+
+    @After
+    public void tearDown()
+        throws Exception
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        if ( httpServer != null )
+        {
+            httpServer.stop();
+            httpServer = null;
+        }
+        factory = null;
+        session = null;
+    }
+
+    @Test
+    public void testClassify()
+    {
+        assertEquals( Transporter.ERROR_OTHER, transporter.classify( new FileNotFoundException() ) );
+        assertEquals( Transporter.ERROR_OTHER, transporter.classify( new HttpResponseException( 403, "Forbidden" ) ) );
+        assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new HttpResponseException( 404, "Not Found" ) ) );
+    }
+
+    @Test
+    public void testPeek()
+        throws Exception
+    {
+        transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "repo/missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 404, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "repo/missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_Authenticated()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_Unauthenticated()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 401, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_ProxyAuthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth );
+        newTransporter( "http://bad.localhost:1/" );
+        transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_ProxyUnauthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort() );
+        newTransporter( "http://bad.localhost:1/" );
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 407, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_SSL()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        newTransporter( httpServer.getHttpsUrl() );
+        transporter.peek( new PeekTask( URI.create( "repo/file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_Redirect()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        transporter.peek( new PeekTask( URI.create( "redirect/file.txt" ) ) );
+        transporter.peek( new PeekTask( URI.create( "redirect/file.txt?scheme=https" ) ) );
+    }
+
+    @Test
+    public void testGet_ToMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ToFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "test", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EmptyResource()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/empty.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EncodedResourcePath()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "repo/some%20space.txt" ) );
+        transporter.get( task );
+        assertEquals( "space", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_Authenticated()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_Unauthenticated()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 401, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_ProxyAuthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        Authentication auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth );
+        newTransporter( "http://bad.localhost:1/" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ProxyUnauthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort() );
+        newTransporter( "http://bad.localhost:1/" );
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 407, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_SSL()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        newTransporter( httpServer.getHttpsUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_WebDav()
+        throws Exception
+    {
+        httpServer.setWebDav( true );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/dir/file.txt" ) ).setListener( listener );
+        ( (HttpTransporter) transporter ).getState().setWebDav( true );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+        assertEquals( httpServer.getLogEntries().toString(), 1, httpServer.getLogEntries().size() );
+    }
+
+    @Test
+    public void testGet_Redirect()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "redirect/file.txt?scheme=https" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_Resume()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "re" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/resume.txt" ) ).setDataFile( file, true ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "resumable", TestFileUtils.readString( file ) );
+        assertEquals( 1L, listener.startedCount );
+        assertEquals( 2L, listener.dataOffset );
+        assertEquals( 9, listener.dataLength );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "sumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ResumeLocalContentsOutdated()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "re" );
+        file.setLastModified( System.currentTimeMillis() - 5 * 60 * 1000 );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/resume.txt" ) ).setDataFile( file, true ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "resumable", TestFileUtils.readString( file ) );
+        assertEquals( 1L, listener.startedCount );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 9, listener.dataLength );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "resumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ResumeRangesNotSupportedByServer()
+        throws Exception
+    {
+        httpServer.setRangeSupport( false );
+        File file = TestFileUtils.createTempFile( "re" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "repo/resume.txt" ) ).setDataFile( file, true ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "resumable", TestFileUtils.readString( file ) );
+        assertEquals( 1L, listener.startedCount );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 9, listener.dataLength );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "resumable", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_Checksums_Nexus()
+        throws Exception
+    {
+        httpServer.setChecksumHeader( HttpServer.ChecksumHeader.NEXUS );
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get( "SHA-1" ) );
+    }
+
+    @Test
+    public void testGet_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File file = TestFileUtils.createTempFile( "failure" );
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ).setDataFile( file ) );
+            assertTrue( i + ", " + file.getAbsolutePath(), file.delete() );
+        }
+    }
+
+    @Test
+    public void testGet_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 404, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testGet_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_FromMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_FromFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "upload" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataFile( file );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_EmptyResource()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_EncodedResourcePath()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task =
+            new PutTask( URI.create( "repo/some%20space.txt" ) ).setListener( listener ).setDataString( "OK" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 2L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "OK", TestFileUtils.readString( new File( repoDir, "some space.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_Authenticated_ExpectContinue()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_Authenticated_ExpectContinueBroken()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        httpServer.setExpectSupport( HttpServer.ExpectContinue.BROKEN );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_Authenticated_ExpectContinueRejected()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        httpServer.setExpectSupport( HttpServer.ExpectContinue.FAIL );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader()
+        throws Exception
+    {
+        Map<String, String> headers = new HashMap<String, String>();
+        headers.put( "Expect", "100-continue" );
+        session.setConfigProperty( ConfigurationProperties.HTTP_HEADERS + ".test", headers );
+        httpServer.setAuthentication( "testuser", "testpass" );
+        httpServer.setExpectSupport( HttpServer.ExpectContinue.FAIL );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_Unauthenticated()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 401, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_ProxyAuthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        Authentication auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth );
+        newTransporter( "http://bad.localhost:1/" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_ProxyUnauthenticated()
+        throws Exception
+    {
+        httpServer.setProxyAuthentication( "testuser", "testpass" );
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort() );
+        newTransporter( "http://bad.localhost:1/" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 407, e.getStatusCode() );
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_SSL()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpsUrl() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPut_WebDav()
+        throws Exception
+    {
+        httpServer.setWebDav( true );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task =
+            new PutTask( URI.create( "repo/dir1/dir2/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", TestFileUtils.readString( new File( repoDir, "dir1/dir2/file.txt" ) ) );
+
+        assertEquals( 5, httpServer.getLogEntries().size() );
+        assertEquals( "OPTIONS", httpServer.getLogEntries().get( 0 ).method );
+        assertEquals( "MKCOL", httpServer.getLogEntries().get( 1 ).method );
+        assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 1 ).path );
+        assertEquals( "MKCOL", httpServer.getLogEntries().get( 2 ).method );
+        assertEquals( "/repo/dir1/", httpServer.getLogEntries().get( 2 ).path );
+        assertEquals( "MKCOL", httpServer.getLogEntries().get( 3 ).method );
+        assertEquals( "/repo/dir1/dir2/", httpServer.getLogEntries().get( 3 ).path );
+        assertEquals( "PUT", httpServer.getLogEntries().get( 4 ).method );
+    }
+
+    @Test
+    public void testPut_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File src = TestFileUtils.createTempFile( "upload" );
+            File dst = new File( repoDir, "file.txt" );
+            transporter.put( new PutTask( URI.create( "repo/file.txt" ) ).setDataFile( src ) );
+            assertTrue( i + ", " + src.getAbsolutePath(), src.delete() );
+            assertTrue( i + ", " + dst.getAbsolutePath(), dst.delete() );
+        }
+    }
+
+    @Test
+    public void testPut_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.put( new PutTask( URI.create( "repo/missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPut_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test
+    public void testGetPut_AuthCache()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        GetTask get = new GetTask( URI.create( "repo/file.txt" ) );
+        transporter.get( get );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "repo/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 1, listener.startedCount );
+    }
+
+    @Test( timeout = 10000L )
+    public void testConcurrency()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        newTransporter( httpServer.getHttpUrl() );
+        final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
+        Thread threads[] = new Thread[20];
+        for ( int i = 0; i < threads.length; i++ )
+        {
+            final String path = "repo/file.txt?i=" + i;
+            threads[i] = new Thread()
+            {
+                @Override
+                public void run()
+                {
+                    try
+                    {
+                        for ( int j = 0; j < 100; j++ )
+                        {
+                            GetTask task = new GetTask( URI.create( path ) );
+                            transporter.get( task );
+                            assertEquals( "test", task.getDataString() );
+                        }
+                    }
+                    catch ( Throwable t )
+                    {
+                        error.compareAndSet( null, t );
+                        System.err.println( path );
+                        t.printStackTrace();
+                    }
+                }
+            };
+            threads[i].setName( "Task-" + i );
+        }
+        for ( Thread thread : threads )
+        {
+            thread.start();
+        }
+        for ( Thread thread : threads )
+        {
+            thread.join();
+        }
+        assertNull( String.valueOf( error.get() ), error.get() );
+    }
+
+    @Test( timeout = 1000L )
+    public void testConnectTimeout()
+        throws Exception
+    {
+        session.setConfigProperty( ConfigurationProperties.CONNECT_TIMEOUT, 100 );
+        int port = 1;
+        newTransporter( "http://localhost:" + port );
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ConnectTimeoutException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        catch ( ConnectException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test( timeout = 1000L )
+    public void testRequestTimeout()
+        throws Exception
+    {
+        session.setConfigProperty( ConfigurationProperties.REQUEST_TIMEOUT, 100 );
+        ServerSocket server = new ServerSocket( 0 );
+        newTransporter( "http://localhost:" + server.getLocalPort() );
+        try
+        {
+            try
+            {
+                transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+                fail( "Expected error" );
+            }
+            catch ( SocketTimeoutException e )
+            {
+                assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+            }
+        }
+        finally
+        {
+            server.close();
+        }
+    }
+
+    @Test
+    public void testUserAgent()
+        throws Exception
+    {
+        session.setConfigProperty( ConfigurationProperties.USER_AGENT, "SomeTest/1.0" );
+        newTransporter( httpServer.getHttpUrl() );
+        transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+        assertEquals( 1, httpServer.getLogEntries().size() );
+        for ( HttpServer.LogEntry log : httpServer.getLogEntries() )
+        {
+            assertEquals( "SomeTest/1.0", log.headers.get( "User-Agent" ) );
+        }
+    }
+
+    @Test
+    public void testCustomHeaders()
+        throws Exception
+    {
+        Map<String, String> headers = new HashMap<String, String>();
+        headers.put( "User-Agent", "Custom/1.0" );
+        headers.put( "X-CustomHeader", "Custom-Value" );
+        session.setConfigProperty( ConfigurationProperties.USER_AGENT, "SomeTest/1.0" );
+        session.setConfigProperty( ConfigurationProperties.HTTP_HEADERS + ".test", headers );
+        newTransporter( httpServer.getHttpUrl() );
+        transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+        assertEquals( 1, httpServer.getLogEntries().size() );
+        for ( HttpServer.LogEntry log : httpServer.getLogEntries() )
+        {
+            for ( Map.Entry<String, String> entry : headers.entrySet() )
+            {
+                assertEquals( entry.getKey(), entry.getValue(), log.headers.get( entry.getKey() ) );
+            }
+        }
+    }
+
+    @Test
+    public void testServerAuthScope_NotUsedForProxy()
+        throws Exception
+    {
+        String username = "testuser", password = "testpass";
+        httpServer.setProxyAuthentication( username, password );
+        auth = new AuthenticationBuilder().addUsername( username ).addPassword( password ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort() );
+        newTransporter( "http://" + httpServer.getHost() + ":12/" );
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Server auth must not be used as proxy auth" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 407, e.getStatusCode() );
+        }
+    }
+
+    @Test
+    public void testProxyAuthScope_NotUsedForServer()
+        throws Exception
+    {
+        String username = "testuser", password = "testpass";
+        httpServer.setAuthentication( username, password );
+        Authentication auth = new AuthenticationBuilder().addUsername( username ).addPassword( password ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth );
+        newTransporter( "http://" + httpServer.getHost() + ":12/" );
+        try
+        {
+            transporter.get( new GetTask( URI.create( "repo/file.txt" ) ) );
+            fail( "Proxy auth must not be used as server auth" );
+        }
+        catch ( HttpResponseException e )
+        {
+            assertEquals( 401, e.getStatusCode() );
+        }
+    }
+
+    @Test
+    public void testAuthSchemeReuse()
+        throws Exception
+    {
+        httpServer.setAuthentication( "testuser", "testpass" );
+        httpServer.setProxyAuthentication( "proxyuser", "proxypass" );
+        session.setCache( new DefaultRepositoryCache() );
+        auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        Authentication auth = new AuthenticationBuilder().addUsername( "proxyuser" ).addPassword( "proxypass" ).build();
+        proxy = new Proxy( Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth );
+        newTransporter( "http://bad.localhost:1/" );
+        GetTask task = new GetTask( URI.create( "repo/file.txt" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 3, httpServer.getLogEntries().size() );
+        httpServer.getLogEntries().clear();
+        newTransporter( "http://bad.localhost:1/" );
+        task = new GetTask( URI.create( "repo/file.txt" ) );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 1, httpServer.getLogEntries().size() );
+        assertNotNull( httpServer.getLogEntries().get( 0 ).headers.get( "Authorization" ) );
+        assertNotNull( httpServer.getLogEntries().get( 0 ).headers.get( "Proxy-Authorization" ) );
+    }
+
+    @Test
+    public void testConnectionReuse()
+        throws Exception
+    {
+        httpServer.addSslConnector();
+        session.setCache( new DefaultRepositoryCache() );
+        for ( int i = 0; i < 3; i++ )
+        {
+            newTransporter( httpServer.getHttpsUrl() );
+            GetTask task = new GetTask( URI.create( "repo/file.txt" ) );
+            transporter.get( task );
+            assertEquals( "test", task.getDataString() );
+        }
+        PoolStats stats =
+            ( (ConnPoolControl<?>) ( (HttpTransporter) transporter ).getState().getConnectionManager() ).getTotalStats();
+        assertEquals( stats.toString(), 1, stats.getAvailable() );
+    }
+
+    @Test( expected = NoTransporterException.class )
+    public void testInit_BadProtocol()
+        throws Exception
+    {
+        newTransporter( "bad:/void" );
+    }
+
+    @Test( expected = NoTransporterException.class )
+    public void testInit_BadUrl()
+        throws Exception
+    {
+        newTransporter( "http://localhost:NaN" );
+    }
+
+    @Test
+    public void testInit_CaseInsensitiveProtocol()
+        throws Exception
+    {
+        newTransporter( "http://localhost" );
+        newTransporter( "HTTP://localhost" );
+        newTransporter( "Http://localhost" );
+        newTransporter( "https://localhost" );
+        newTransporter( "HTTPS://localhost" );
+        newTransporter( "HttpS://localhost" );
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java
new file mode 100644
index 0000000..d88a320
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/RecordingTransportListener.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+class RecordingTransportListener
+    extends TransportListener
+{
+
+    public final ByteArrayOutputStream baos = new ByteArrayOutputStream( 1024 );
+
+    public long dataOffset;
+
+    public long dataLength;
+
+    public int startedCount;
+
+    public int progressedCount;
+
+    public boolean cancelStart;
+
+    public boolean cancelProgress;
+
+    @Override
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+        startedCount++;
+        progressedCount = 0;
+        this.dataLength = dataLength;
+        this.dataOffset = dataOffset;
+        baos.reset();
+        if ( cancelStart )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+    @Override
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+        progressedCount++;
+        baos.write( data.array(), data.arrayOffset() + data.position(), data.remaining() );
+        if ( cancelProgress )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/UriUtilsTest.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/UriUtilsTest.java
new file mode 100644
index 0000000..e3ea9fa
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/UriUtilsTest.java
@@ -0,0 +1,137 @@
+package org.eclipse.aether.transport.http;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+
+public class UriUtilsTest
+{
+
+    private String resolve( URI base, String ref )
+    {
+        return UriUtils.resolve( base, URI.create( ref ) ).toString();
+    }
+
+    @Test
+    public void testResolve_BaseEmptyPath()
+    {
+        URI base = URI.create( "http://host" );
+        assertEquals( "http://host/file.jar", resolve( base, "file.jar" ) );
+        assertEquals( "http://host/dir/file.jar", resolve( base, "dir/file.jar" ) );
+        assertEquals( "http://host?arg=val", resolve( base, "?arg=val" ) );
+        assertEquals( "http://host/file?arg=val", resolve( base, "file?arg=val" ) );
+        assertEquals( "http://host/dir/file?arg=val", resolve( base, "dir/file?arg=val" ) );
+    }
+
+    @Test
+    public void testResolve_BaseRootPath()
+    {
+        URI base = URI.create( "http://host/" );
+        assertEquals( "http://host/file.jar", resolve( base, "file.jar" ) );
+        assertEquals( "http://host/dir/file.jar", resolve( base, "dir/file.jar" ) );
+        assertEquals( "http://host/?arg=val", resolve( base, "?arg=val" ) );
+        assertEquals( "http://host/file?arg=val", resolve( base, "file?arg=val" ) );
+        assertEquals( "http://host/dir/file?arg=val", resolve( base, "dir/file?arg=val" ) );
+    }
+
+    @Test
+    public void testResolve_BasePathTrailingSlash()
+    {
+        URI base = URI.create( "http://host/sub/dir/" );
+        assertEquals( "http://host/sub/dir/file.jar", resolve( base, "file.jar" ) );
+        assertEquals( "http://host/sub/dir/dir/file.jar", resolve( base, "dir/file.jar" ) );
+        assertEquals( "http://host/sub/dir/?arg=val", resolve( base, "?arg=val" ) );
+        assertEquals( "http://host/sub/dir/file?arg=val", resolve( base, "file?arg=val" ) );
+        assertEquals( "http://host/sub/dir/dir/file?arg=val", resolve( base, "dir/file?arg=val" ) );
+    }
+
+    @Test
+    public void testResolve_BasePathNoTrailingSlash()
+    {
+        URI base = URI.create( "http://host/sub/d%20r" );
+        assertEquals( "http://host/sub/d%20r/file.jar", resolve( base, "file.jar" ) );
+        assertEquals( "http://host/sub/d%20r/dir/file.jar", resolve( base, "dir/file.jar" ) );
+        assertEquals( "http://host/sub/d%20r?arg=val", resolve( base, "?arg=val" ) );
+        assertEquals( "http://host/sub/d%20r/file?arg=val", resolve( base, "file?arg=val" ) );
+        assertEquals( "http://host/sub/d%20r/dir/file?arg=val", resolve( base, "dir/file?arg=val" ) );
+    }
+
+    private List<URI> getDirs( String base, String uri )
+    {
+        return UriUtils.getDirectories( ( base != null ) ? URI.create( base ) : null, URI.create( uri ) );
+    }
+
+    private void assertUris( List<URI> actual, String... expected )
+    {
+        List<String> uris = new ArrayList<String>( actual.size() );
+        for ( URI uri : actual )
+        {
+            uris.add( uri.toString() );
+        }
+        assertEquals( Arrays.asList( expected ), uris );
+    }
+
+    @Test
+    public void testGetDirectories_NoBase()
+    {
+        List<URI> parents = getDirs( null, "http://host/repo/sub/dir/file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/", "http://host/repo/" );
+
+        parents = getDirs( null, "http://host/repo/sub/dir/?file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/", "http://host/repo/" );
+
+        parents = getDirs( null, "http://host/" );
+        assertUris( parents );
+    }
+
+    @Test
+    public void testGetDirectories_ExplicitBaseTrailingSlash()
+    {
+        List<URI> parents = getDirs( "http://host/repo/", "http://host/repo/sub/dir/file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/" );
+
+        parents = getDirs( "http://host/repo/", "http://host/repo/sub/dir/?file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/" );
+
+        parents = getDirs( "http://host/repo/", "http://host/" );
+        assertUris( parents );
+    }
+
+    @Test
+    public void testGetDirectories_ExplicitBaseNoTrailingSlash()
+    {
+        List<URI> parents = getDirs( "http://host/repo", "http://host/repo/sub/dir/file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/" );
+
+        parents = getDirs( "http://host/repo", "http://host/repo/sub/dir/?file.jar" );
+        assertUris( parents, "http://host/repo/sub/dir/", "http://host/repo/sub/" );
+
+        parents = getDirs( "http://host/repo", "http://host/" );
+        assertUris( parents );
+    }
+
+}
diff --git a/maven-resolver-transport-http/src/test/resources/logback.xml b/maven-resolver-transport-http/src/test/resources/logback.xml
new file mode 100644
index 0000000..9addbd5
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/resources/logback.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ ! 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.
+ !-->
+
+<configuration>
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="DEBUG">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+  <logger name="org.apache.http.wire" level="DEBUG" />
+  <logger name="org.eclipse.jetty" level="INFO" />
+</configuration>
diff --git a/maven-resolver-transport-http/src/test/resources/ssl/README.txt b/maven-resolver-transport-http/src/test/resources/ssl/README.txt
new file mode 100644
index 0000000..b1be71c
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/resources/ssl/README.txt
@@ -0,0 +1,5 @@
+client-store generated via
+> keytool -genkey -alias localhost -keypass client-pwd -keystore client-store -storepass client-pwd -validity 4096 -dname "cn=localhost, ou=None, L=Seattle, ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
+
+server-store generated via
+> keytool -genkey -alias localhost -keypass server-pwd -keystore server-store -storepass server-pwd -validity 4096 -dname "cn=localhost, ou=None, L=Seattle, ST=Washington, o=ExampleOrg, c=US" -keyalg RSA
diff --git a/maven-resolver-transport-http/src/test/resources/ssl/client-store b/maven-resolver-transport-http/src/test/resources/ssl/client-store
new file mode 100644
index 0000000..fbfb39d
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/resources/ssl/client-store
Binary files differ
diff --git a/maven-resolver-transport-http/src/test/resources/ssl/server-store b/maven-resolver-transport-http/src/test/resources/ssl/server-store
new file mode 100644
index 0000000..6137fee
--- /dev/null
+++ b/maven-resolver-transport-http/src/test/resources/ssl/server-store
Binary files differ
diff --git a/maven-resolver-transport-wagon/pom.xml b/maven-resolver-transport-wagon/pom.xml
new file mode 100644
index 0000000..460b5fc
--- /dev/null
+++ b/maven-resolver-transport-wagon/pom.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-transport-wagon</artifactId>
+
+  <name>Maven Artifact Resolver Transport Wagon</name>
+  <description>
+    A transport implementation based on Maven Wagon.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.transport.wagon</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-spi</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.wagon</groupId>
+      <artifactId>wagon-provider-api</artifactId>
+      <version>3.0.0</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.inject</groupId>
+      <artifactId>javax.inject</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.plexus</groupId>
+      <artifactId>plexus-component-annotations</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.plexus</groupId>
+      <artifactId>plexus-classworlds</artifactId>
+      <version>2.5.2</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.plexus</groupId>
+      <artifactId>plexus-utils</artifactId>
+      <version>3.0.24</version>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.sisu</groupId>
+      <artifactId>org.eclipse.sisu.plexus</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.sonatype.sisu</groupId>
+      <artifactId>sisu-guice</artifactId>
+      <classifier>no_aop</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.plexus</groupId>
+        <artifactId>plexus-component-metadata</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>sisu-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonConfigurator.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonConfigurator.java
new file mode 100644
index 0000000..7fe22b8
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonConfigurator.java
@@ -0,0 +1,115 @@
+package org.eclipse.aether.internal.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.maven.wagon.Wagon;
+import org.codehaus.plexus.PlexusContainer;
+import org.codehaus.plexus.classworlds.realm.ClassRealm;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.component.configurator.AbstractComponentConfigurator;
+import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
+import org.codehaus.plexus.component.configurator.ConfigurationListener;
+import org.codehaus.plexus.component.configurator.converters.composite.ObjectWithFieldsConverter;
+import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
+import org.codehaus.plexus.configuration.PlexusConfiguration;
+import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+import org.eclipse.aether.transport.wagon.WagonConfigurator;
+
+/**
+ * A wagon configurator based on the Plexus component configuration framework.
+ */
+@Component( role = WagonConfigurator.class, hint = "plexus" )
+public class PlexusWagonConfigurator
+    implements WagonConfigurator
+{
+
+    @Requirement
+    private PlexusContainer container;
+
+    /**
+     * Creates an uninitialized wagon configurator.
+     * 
+     * @noreference This constructor only supports the Plexus IoC container and should not be called directly by
+     *              clients.
+     */
+    public PlexusWagonConfigurator()
+    {
+        // enables no-arg constructor
+    }
+
+    /**
+     * Creates a wagon configurator using the specified Plexus container.
+     *
+     * @param container The Plexus container instance to use, must not be {@code null}.
+     */
+    public PlexusWagonConfigurator( PlexusContainer container )
+    {
+        this.container = requireNonNull( container, "plexus container cannot be null" );
+    }
+
+    public void configure( Wagon wagon, Object configuration )
+        throws Exception
+    {
+        PlexusConfiguration config = null;
+        if ( configuration instanceof PlexusConfiguration )
+        {
+            config = (PlexusConfiguration) configuration;
+        }
+        else if ( configuration instanceof Xpp3Dom )
+        {
+            config = new XmlPlexusConfiguration( (Xpp3Dom) configuration );
+        }
+        else if ( configuration == null )
+        {
+            return;
+        }
+        else
+        {
+            throw new IllegalArgumentException( "unexpected configuration type: " + configuration.getClass().getName() );
+        }
+
+        WagonComponentConfigurator configurator = new WagonComponentConfigurator();
+
+        configurator.configureComponent( wagon, config, container.getContainerRealm() );
+    }
+
+    static class WagonComponentConfigurator
+        extends AbstractComponentConfigurator
+    {
+
+        @Override
+        public void configureComponent( Object component, PlexusConfiguration configuration,
+                                        ExpressionEvaluator expressionEvaluator, ClassRealm containerRealm,
+                                        ConfigurationListener listener )
+            throws ComponentConfigurationException
+        {
+            ObjectWithFieldsConverter converter = new ObjectWithFieldsConverter();
+
+            converter.processConfiguration( converterLookup, component, containerRealm, configuration,
+                                            expressionEvaluator, listener );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonProvider.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonProvider.java
new file mode 100644
index 0000000..d534695
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/PlexusWagonProvider.java
@@ -0,0 +1,83 @@
+package org.eclipse.aether.internal.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.maven.wagon.Wagon;
+import org.codehaus.plexus.PlexusContainer;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.eclipse.aether.transport.wagon.WagonProvider;
+
+/**
+ * A wagon provider backed by a Plexus container and the wagons registered with this container.
+ */
+@Component( role = WagonProvider.class, hint = "plexus" )
+public class PlexusWagonProvider
+    implements WagonProvider
+{
+
+    @Requirement
+    private PlexusContainer container;
+
+    /**
+     * Creates an uninitialized wagon provider.
+     * 
+     * @noreference This constructor only supports the Plexus IoC container and should not be called directly by
+     *              clients.
+     */
+    public PlexusWagonProvider()
+    {
+        // enables no-arg constructor
+    }
+
+    /**
+     * Creates a wagon provider using the specified Plexus container.
+     *
+     * @param container The Plexus container instance to use, must not be {@code null}.
+     */
+    public PlexusWagonProvider( PlexusContainer container )
+    {
+        this.container = requireNonNull( container, "plexus container cannot be null" );
+    }
+
+    public Wagon lookup( String roleHint )
+        throws Exception
+    {
+        return container.lookup( Wagon.class, roleHint );
+    }
+
+    public void release( Wagon wagon )
+    {
+        try
+        {
+            if ( wagon != null )
+            {
+                container.release( wagon );
+            }
+        }
+        catch ( Exception e )
+        {
+            // too bad
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/package-info.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/package-info.java
new file mode 100644
index 0000000..df14e9c
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/internal/transport/wagon/package-info.java
@@ -0,0 +1,25 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Integration with the Plexus IoC container which is the native runtime environment expected by many wagon
+ * implementations.
+ */
+package org.eclipse.aether.internal.transport.wagon;
+
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonCancelledException.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonCancelledException.java
new file mode 100644
index 0000000..105917f
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonCancelledException.java
@@ -0,0 +1,45 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+/**
+ * Unchecked exception to allow the checked {@link TransferCancelledException} to bubble up from a wagon.
+ */
+class WagonCancelledException
+    extends RuntimeException
+{
+
+    public WagonCancelledException( TransferCancelledException cause )
+    {
+        super( cause );
+    }
+
+    public static Exception unwrap( Exception e )
+    {
+        if ( e instanceof WagonCancelledException )
+        {
+            e = (Exception) e.getCause();
+        }
+        return e;
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonConfigurator.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonConfigurator.java
new file mode 100644
index 0000000..42399cb
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonConfigurator.java
@@ -0,0 +1,40 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.apache.maven.wagon.Wagon;
+
+/**
+ * A component to configure wagon instances with provider-specific parameters.
+ */
+public interface WagonConfigurator
+{
+
+    /**
+     * Configures the specified wagon instance with the given configuration.
+     * 
+     * @param wagon The wagon instance to configure, must not be {@code null}.
+     * @param configuration The configuration to apply to the wagon instance, must not be {@code null}.
+     * @throws Exception If the configuration could not be applied to the wagon.
+     */
+    void configure( Wagon wagon, Object configuration )
+        throws Exception;
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonProvider.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonProvider.java
new file mode 100644
index 0000000..77bf9d6
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonProvider.java
@@ -0,0 +1,49 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.apache.maven.wagon.Wagon;
+
+/**
+ * A component to acquire and release wagon instances for uploads/downloads.
+ */
+public interface WagonProvider
+{
+
+    /**
+     * Acquires a wagon instance that matches the specified role hint. The role hint is derived from the URI scheme,
+     * e.g. "http" or "file".
+     * 
+     * @param roleHint The role hint to get a wagon for, must not be {@code null}.
+     * @return The requested wagon instance, never {@code null}.
+     * @throws Exception If no wagon could be retrieved for the specified role hint.
+     */
+    Wagon lookup( String roleHint )
+        throws Exception;
+
+    /**
+     * Releases the specified wagon. A wagon provider may either free any resources allocated for the wagon instance or
+     * return the instance back to a pool for future use.
+     * 
+     * @param wagon The wagon to release, may be {@code null}.
+     */
+    void release( Wagon wagon );
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransferListener.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransferListener.java
new file mode 100644
index 0000000..3c3120e
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransferListener.java
@@ -0,0 +1,72 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.nio.ByteBuffer;
+
+import org.apache.maven.wagon.events.TransferEvent;
+import org.apache.maven.wagon.observers.AbstractTransferListener;
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+/**
+ * A wagon transfer listener that forwards events to a transport listener.
+ */
+final class WagonTransferListener
+    extends AbstractTransferListener
+{
+
+    private final TransportListener listener;
+
+    public WagonTransferListener( TransportListener listener )
+    {
+        this.listener = listener;
+    }
+
+    @Override
+    public void transferStarted( TransferEvent event )
+    {
+        try
+        {
+            listener.transportStarted( 0, event.getResource().getContentLength() );
+        }
+        catch ( TransferCancelledException e )
+        {
+            /*
+             * NOTE: Wagon transfers are not freely abortable. In particular, aborting from
+             * AbstractWagon.fire(Get|Put)Started() would result in unclosed streams so we avoid this case.
+             */
+        }
+    }
+
+    @Override
+    public void transferProgress( TransferEvent event, byte[] buffer, int length )
+    {
+        try
+        {
+            listener.transportProgressed( ByteBuffer.wrap( buffer, 0, length ) );
+        }
+        catch ( TransferCancelledException e )
+        {
+            throw new WagonCancelledException( e );
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java
new file mode 100644
index 0000000..e8a4049
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java
@@ -0,0 +1,753 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Queue;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.StreamingWagon;
+import org.apache.maven.wagon.Wagon;
+import org.apache.maven.wagon.authentication.AuthenticationInfo;
+import org.apache.maven.wagon.proxy.ProxyInfo;
+import org.apache.maven.wagon.proxy.ProxyInfoProvider;
+import org.apache.maven.wagon.repository.Repository;
+import org.apache.maven.wagon.repository.RepositoryPermissions;
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.TransportTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * A transporter using Maven Wagon.
+ */
+final class WagonTransporter
+    implements Transporter
+{
+
+    private static final String CONFIG_PROP_CONFIG = "aether.connector.wagon.config";
+
+    private static final String CONFIG_PROP_FILE_MODE = "aether.connector.perms.fileMode";
+
+    private static final String CONFIG_PROP_DIR_MODE = "aether.connector.perms.dirMode";
+
+    private static final String CONFIG_PROP_GROUP = "aether.connector.perms.group";
+
+    private final Logger logger;
+
+    private final RemoteRepository repository;
+
+    private final RepositorySystemSession session;
+
+    private final AuthenticationContext repoAuthContext;
+
+    private final AuthenticationContext proxyAuthContext;
+
+    private final WagonProvider wagonProvider;
+
+    private final WagonConfigurator wagonConfigurator;
+
+    private final String wagonHint;
+
+    private final Repository wagonRepo;
+
+    private final AuthenticationInfo wagonAuth;
+
+    private final ProxyInfoProvider wagonProxy;
+
+    private final Properties headers;
+
+    private final Queue<Wagon> wagons = new ConcurrentLinkedQueue<Wagon>();
+
+    private final AtomicBoolean closed = new AtomicBoolean();
+
+    public WagonTransporter( WagonProvider wagonProvider, WagonConfigurator wagonConfigurator,
+                             RemoteRepository repository, RepositorySystemSession session, Logger logger )
+        throws NoTransporterException
+    {
+        this.logger = logger;
+        this.wagonProvider = wagonProvider;
+        this.wagonConfigurator = wagonConfigurator;
+        this.repository = repository;
+        this.session = session;
+
+        wagonRepo = new Repository( repository.getId(), repository.getUrl() );
+        wagonRepo.setPermissions( getPermissions( repository.getId(), session ) );
+
+        wagonHint = wagonRepo.getProtocol().toLowerCase( Locale.ENGLISH );
+        if ( wagonHint == null || wagonHint.length() <= 0 )
+        {
+            throw new NoTransporterException( repository );
+        }
+
+        try
+        {
+            wagons.add( lookupWagon() );
+        }
+        catch ( Exception e )
+        {
+            logger.debug( e.getMessage(), e );
+            throw new NoTransporterException( repository, e.getMessage(), e );
+        }
+
+        repoAuthContext = AuthenticationContext.forRepository( session, repository );
+        proxyAuthContext = AuthenticationContext.forProxy( session, repository );
+
+        wagonAuth = getAuthenticationInfo( repository, repoAuthContext );
+        wagonProxy = getProxy( repository, proxyAuthContext );
+
+        headers = new Properties();
+        headers.put( "User-Agent", ConfigUtils.getString( session, ConfigurationProperties.DEFAULT_USER_AGENT,
+                                                          ConfigurationProperties.USER_AGENT ) );
+        Map<?, ?> headers =
+            ConfigUtils.getMap( session, null, ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
+                                ConfigurationProperties.HTTP_HEADERS );
+        if ( headers != null )
+        {
+            this.headers.putAll( headers );
+        }
+    }
+
+    private static RepositoryPermissions getPermissions( String repoId, RepositorySystemSession session )
+    {
+        RepositoryPermissions result = null;
+
+        RepositoryPermissions perms = new RepositoryPermissions();
+
+        String suffix = '.' + repoId;
+
+        String fileMode = ConfigUtils.getString( session, (String) null, CONFIG_PROP_FILE_MODE + suffix );
+        if ( fileMode != null )
+        {
+            perms.setFileMode( fileMode );
+            result = perms;
+        }
+
+        String dirMode = ConfigUtils.getString( session, (String) null, CONFIG_PROP_DIR_MODE + suffix );
+        if ( dirMode != null )
+        {
+            perms.setDirectoryMode( dirMode );
+            result = perms;
+        }
+
+        String group = ConfigUtils.getString( session, (String) null, CONFIG_PROP_GROUP + suffix );
+        if ( group != null )
+        {
+            perms.setGroup( group );
+            result = perms;
+        }
+
+        return result;
+    }
+
+    private AuthenticationInfo getAuthenticationInfo( RemoteRepository repository,
+                                                      final AuthenticationContext authContext )
+    {
+        AuthenticationInfo auth = null;
+
+        if ( authContext != null )
+        {
+            auth = new AuthenticationInfo()
+            {
+                @Override
+                public String getUserName()
+                {
+                    return authContext.get( AuthenticationContext.USERNAME );
+                }
+
+                @Override
+                public String getPassword()
+                {
+                    return authContext.get( AuthenticationContext.PASSWORD );
+                }
+
+                @Override
+                public String getPrivateKey()
+                {
+                    return authContext.get( AuthenticationContext.PRIVATE_KEY_PATH );
+                }
+
+                @Override
+                public String getPassphrase()
+                {
+                    return authContext.get( AuthenticationContext.PRIVATE_KEY_PASSPHRASE );
+                }
+            };
+        }
+
+        return auth;
+    }
+
+    private ProxyInfoProvider getProxy( RemoteRepository repository, final AuthenticationContext authContext )
+    {
+        ProxyInfoProvider proxy = null;
+
+        Proxy p = repository.getProxy();
+        if ( p != null )
+        {
+            final ProxyInfo prox;
+            if ( authContext != null )
+            {
+                prox = new ProxyInfo()
+                {
+                    @Override
+                    public String getUserName()
+                    {
+                        return authContext.get( AuthenticationContext.USERNAME );
+                    }
+
+                    @Override
+                    public String getPassword()
+                    {
+                        return authContext.get( AuthenticationContext.PASSWORD );
+                    }
+
+                    @Override
+                    public String getNtlmDomain()
+                    {
+                        return authContext.get( AuthenticationContext.NTLM_DOMAIN );
+                    }
+
+                    @Override
+                    public String getNtlmHost()
+                    {
+                        return authContext.get( AuthenticationContext.NTLM_WORKSTATION );
+                    }
+                };
+            }
+            else
+            {
+                prox = new ProxyInfo();
+            }
+            prox.setType( p.getType() );
+            prox.setHost( p.getHost() );
+            prox.setPort( p.getPort() );
+
+            proxy = new ProxyInfoProvider()
+            {
+                public ProxyInfo getProxyInfo( String protocol )
+                {
+                    return prox;
+                }
+            };
+        }
+
+        return proxy;
+    }
+
+    private Wagon lookupWagon()
+        throws Exception
+    {
+        return wagonProvider.lookup( wagonHint );
+    }
+
+    private void releaseWagon( Wagon wagon )
+    {
+        wagonProvider.release( wagon );
+    }
+
+    private void connectWagon( Wagon wagon )
+        throws Exception
+    {
+        if ( !headers.isEmpty() )
+        {
+            try
+            {
+                Method setHttpHeaders = wagon.getClass().getMethod( "setHttpHeaders", Properties.class );
+                setHttpHeaders.invoke( wagon, headers );
+            }
+            catch ( NoSuchMethodException e )
+            {
+                // normal for non-http wagons
+            }
+            catch ( Exception e )
+            {
+                logger.debug( "Could not set user agent for wagon " + wagon.getClass().getName() + ": " + e );
+            }
+        }
+
+        int connectTimeout =
+            ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
+                                    ConfigurationProperties.CONNECT_TIMEOUT );
+        int requestTimeout =
+            ConfigUtils.getInteger( session, ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
+                                    ConfigurationProperties.REQUEST_TIMEOUT );
+
+        wagon.setTimeout( Math.max( Math.max( connectTimeout, requestTimeout ), 0 ) );
+
+        wagon.setInteractive( ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_INTERACTIVE,
+                                                      ConfigurationProperties.INTERACTIVE ) );
+
+        Object configuration = ConfigUtils.getObject( session, null, CONFIG_PROP_CONFIG + "." + repository.getId() );
+        if ( configuration != null && wagonConfigurator != null )
+        {
+            try
+            {
+                wagonConfigurator.configure( wagon, configuration );
+            }
+            catch ( Exception e )
+            {
+                String msg =
+                    "Could not apply configuration for " + repository.getId() + " to wagon "
+                        + wagon.getClass().getName() + ":" + e.getMessage();
+                if ( logger.isDebugEnabled() )
+                {
+                    logger.warn( msg, e );
+                }
+                else
+                {
+                    logger.warn( msg );
+                }
+            }
+        }
+
+        wagon.connect( wagonRepo, wagonAuth, wagonProxy );
+    }
+
+    private void disconnectWagon( Wagon wagon )
+    {
+        try
+        {
+            if ( wagon != null )
+            {
+                wagon.disconnect();
+            }
+        }
+        catch ( Exception e )
+        {
+            logger.debug( "Could not disconnect wagon " + wagon, e );
+        }
+    }
+
+    private Wagon pollWagon()
+        throws Exception
+    {
+        Wagon wagon = wagons.poll();
+
+        if ( wagon == null )
+        {
+            try
+            {
+                wagon = lookupWagon();
+                connectWagon( wagon );
+            }
+            catch ( Exception e )
+            {
+                releaseWagon( wagon );
+                throw e;
+            }
+        }
+        else if ( wagon.getRepository() == null )
+        {
+            try
+            {
+                connectWagon( wagon );
+            }
+            catch ( Exception e )
+            {
+                wagons.add( wagon );
+                throw e;
+            }
+        }
+
+        return wagon;
+    }
+
+    public int classify( Throwable error )
+    {
+        if ( error instanceof ResourceDoesNotExistException )
+        {
+            return ERROR_NOT_FOUND;
+        }
+        return ERROR_OTHER;
+    }
+
+    public void peek( PeekTask task )
+        throws Exception
+    {
+        execute( task, new PeekTaskRunner( task ) );
+    }
+
+    public void get( GetTask task )
+        throws Exception
+    {
+        execute( task, new GetTaskRunner( task ) );
+    }
+
+    public void put( PutTask task )
+        throws Exception
+    {
+        execute( task, new PutTaskRunner( task ) );
+    }
+
+    private void execute( TransportTask task, TaskRunner runner )
+        throws Exception
+    {
+        if ( closed.get() )
+        {
+            throw new IllegalStateException( "transporter closed, cannot execute task " + task );
+        }
+        try
+        {
+            WagonTransferListener listener = new WagonTransferListener( task.getListener() );
+            Wagon wagon = pollWagon();
+            try
+            {
+                wagon.addTransferListener( listener );
+                runner.run( wagon );
+            }
+            finally
+            {
+                wagon.removeTransferListener( listener );
+                wagons.add( wagon );
+            }
+        }
+        catch ( Exception e )
+        {
+            throw WagonCancelledException.unwrap( e );
+        }
+    }
+
+    private static File newTempFile()
+        throws IOException
+    {
+        return File.createTempFile( "wagon-" + UUID.randomUUID().toString().replace( "-", "" ), ".tmp" );
+    }
+
+    private void delTempFile( File path )
+    {
+        if ( path != null && !path.delete() && path.exists() )
+        {
+            logger.debug( "Could not delete temorary file " + path );
+            path.deleteOnExit();
+        }
+    }
+
+    private static void copy( OutputStream os, InputStream is )
+        throws IOException
+    {
+        byte[] buffer = new byte[1024 * 32];
+        for ( int read = is.read( buffer ); read >= 0; read = is.read( buffer ) )
+        {
+            os.write( buffer, 0, read );
+        }
+    }
+
+    public void close()
+    {
+        if ( closed.compareAndSet( false, true ) )
+        {
+            AuthenticationContext.close( repoAuthContext );
+            AuthenticationContext.close( proxyAuthContext );
+
+            for ( Wagon wagon = wagons.poll(); wagon != null; wagon = wagons.poll() )
+            {
+                disconnectWagon( wagon );
+                releaseWagon( wagon );
+            }
+        }
+    }
+
+    private interface TaskRunner
+    {
+
+        void run( Wagon wagon )
+            throws Exception;
+
+    }
+
+    private static class PeekTaskRunner
+        implements TaskRunner
+    {
+
+        private final PeekTask task;
+
+        public PeekTaskRunner( PeekTask task )
+        {
+            this.task = task;
+        }
+
+        public void run( Wagon wagon )
+            throws Exception
+        {
+            String src = task.getLocation().toString();
+            if ( !wagon.resourceExists( src ) )
+            {
+                throw new ResourceDoesNotExistException( "Could not find " + src + " in "
+                    + wagon.getRepository().getUrl() );
+            }
+        }
+
+    }
+
+    private class GetTaskRunner
+        implements TaskRunner
+    {
+
+        private final GetTask task;
+
+        public GetTaskRunner( GetTask task )
+        {
+            this.task = task;
+        }
+
+        public void run( Wagon wagon )
+            throws Exception
+        {
+            String src = task.getLocation().toString();
+            File file = task.getDataFile();
+            if ( file == null && wagon instanceof StreamingWagon )
+            {
+                OutputStream dst = null;
+                try
+                {
+                    dst = task.newOutputStream();
+                    ( (StreamingWagon) wagon ).getToStream( src, dst );
+                    dst.close();
+                    dst = null;
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( dst != null )
+                        {
+                            dst.close();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                }
+            }
+            else
+            {
+                File dst = ( file != null ) ? file : newTempFile();
+                try
+                {
+                    wagon.get( src, dst );
+                    /*
+                     * NOTE: Wagon (1.0-beta-6) doesn't create the destination file when transferring a 0-byte
+                     * resource. So if the resource we asked for didn't cause any exception but doesn't show up in
+                     * the dst file either, Wagon tells us in its weird way the file is empty.
+                     */
+                    if ( !dst.exists() && !dst.createNewFile() )
+                    {
+                        throw new IOException( String.format( "Failure creating file '%s'.", dst.getAbsolutePath() ) );
+                    }
+                    if ( file == null )
+                    {
+                        readTempFile( dst );
+                    }
+                }
+                finally
+                {
+                    if ( file == null )
+                    {
+                        delTempFile( dst );
+                    }
+                }
+            }
+        }
+
+        private void readTempFile( File dst )
+            throws IOException
+        {
+            FileInputStream in = null;
+            OutputStream out = null;
+            try
+            {
+                in = new FileInputStream( dst );
+                out = task.newOutputStream();
+                copy( out, in );
+                out.close();
+                out = null;
+                in.close();
+                in = null;
+            }
+            finally
+            {
+                try
+                {
+                    if ( out != null )
+                    {
+                        out.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( in != null )
+                        {
+                            in.close();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                }
+            }
+        }
+
+    }
+
+    private class PutTaskRunner
+        implements TaskRunner
+    {
+
+        private final PutTask task;
+
+        public PutTaskRunner( PutTask task )
+        {
+            this.task = task;
+        }
+
+        public void run( Wagon wagon )
+            throws Exception
+        {
+            String dst = task.getLocation().toString();
+            File file = task.getDataFile();
+            if ( file == null && wagon instanceof StreamingWagon )
+            {
+                InputStream src = null;
+                try
+                {
+                    src = task.newInputStream();
+                    // StreamingWagon uses an internal buffer on src input stream.
+                    ( (StreamingWagon) wagon ).putFromStream( src, dst, task.getDataLength(), -1 );
+                    src.close();
+                    src = null;
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( src != null )
+                        {
+                            src.close();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                }
+            }
+            else
+            {
+                File src = ( file != null ) ? file : createTempFile();
+                try
+                {
+                    wagon.put( src, dst );
+                }
+                finally
+                {
+                    if ( file == null )
+                    {
+                        delTempFile( src );
+                    }
+                }
+            }
+        }
+
+        private File createTempFile()
+            throws IOException
+        {
+            File tmp = newTempFile();
+            OutputStream out = null;
+            InputStream in = null;
+            try
+            {
+                in = task.newInputStream();
+                out = new FileOutputStream( tmp );
+                copy( out, in );
+                out.close();
+                out = null;
+                in.close();
+                in = null;
+            }
+            catch ( IOException e )
+            {
+                delTempFile( tmp );
+                throw e;
+            }
+            finally
+            {
+                try
+                {
+                    if ( out != null )
+                    {
+                        out.close();
+                    }
+                }
+                catch ( final IOException e )
+                {
+                    // Suppressed due to an exception already thrown in the try block.
+                }
+                finally
+                {
+                    try
+                    {
+                        if ( in != null )
+                        {
+                            in.close();
+                        }
+                    }
+                    catch ( final IOException e )
+                    {
+                        // Suppressed due to an exception already thrown in the try block.
+                    }
+                }
+            }
+
+            return tmp;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporterFactory.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporterFactory.java
new file mode 100644
index 0000000..bff3406
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporterFactory.java
@@ -0,0 +1,139 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.spi.log.Logger;
+import org.eclipse.aether.spi.log.LoggerFactory;
+import org.eclipse.aether.spi.log.NullLoggerFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+
+/**
+ * A transporter factory using <a href="http://maven.apache.org/wagon/" target="_blank">Apache Maven Wagon</a>. Note
+ * that this factory merely serves as an adapter to the Wagon API and by itself does not provide any transport services
+ * unless one or more wagon implementations are registered with the {@link WagonProvider}.
+ */
+@Named( "wagon" )
+public final class WagonTransporterFactory
+    implements TransporterFactory, Service
+{
+
+    private Logger logger = NullLoggerFactory.LOGGER;
+
+    private WagonProvider wagonProvider;
+
+    private WagonConfigurator wagonConfigurator;
+
+    private float priority = -1.0f;
+
+    /**
+     * Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation
+     * by clients, the new factory needs to be configured via its various mutators before first use or runtime errors
+     * will occur.
+     */
+    public WagonTransporterFactory()
+    {
+        // enables default constructor
+    }
+
+    @Inject
+    WagonTransporterFactory( WagonProvider wagonProvider, WagonConfigurator wagonConfigurator,
+                             LoggerFactory loggerFactory )
+    {
+        setWagonProvider( wagonProvider );
+        setWagonConfigurator( wagonConfigurator );
+        setLoggerFactory( loggerFactory );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setLoggerFactory( locator.getService( LoggerFactory.class ) );
+        setWagonProvider( locator.getService( WagonProvider.class ) );
+        setWagonConfigurator( locator.getService( WagonConfigurator.class ) );
+    }
+
+    /**
+     * Sets the logger factory to use for this component.
+     * 
+     * @param loggerFactory The logger factory to use, may be {@code null} to disable logging.
+     * @return This component for chaining, never {@code null}.
+     */
+    public WagonTransporterFactory setLoggerFactory( LoggerFactory loggerFactory )
+    {
+        this.logger = NullLoggerFactory.getSafeLogger( loggerFactory, WagonTransporter.class );
+        return this;
+    }
+
+    /**
+     * Sets the wagon provider to use to acquire and release wagon instances.
+     * 
+     * @param wagonProvider The wagon provider to use, may be {@code null}.
+     * @return This factory for chaining, never {@code null}.
+     */
+    public WagonTransporterFactory setWagonProvider( WagonProvider wagonProvider )
+    {
+        this.wagonProvider = wagonProvider;
+        return this;
+    }
+
+    /**
+     * Sets the wagon configurator to use to apply provider-specific configuration to wagon instances.
+     * 
+     * @param wagonConfigurator The wagon configurator to use, may be {@code null}.
+     * @return This factory for chaining, never {@code null}.
+     */
+    public WagonTransporterFactory setWagonConfigurator( WagonConfigurator wagonConfigurator )
+    {
+        this.wagonConfigurator = wagonConfigurator;
+        return this;
+    }
+
+    public float getPriority()
+    {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of this component.
+     * 
+     * @param priority The priority.
+     * @return This component for chaining, never {@code null}.
+     */
+    public WagonTransporterFactory setPriority( float priority )
+    {
+        this.priority = priority;
+        return this;
+    }
+
+    public Transporter newInstance( RepositorySystemSession session, RemoteRepository repository )
+        throws NoTransporterException
+    {
+        return new WagonTransporter( wagonProvider, wagonConfigurator, repository, session, logger );
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/package-info.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/package-info.java
new file mode 100644
index 0000000..82df9ac
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Support for downloads/uploads using <a href="http://maven.apache.org/wagon/" target="_blank">Apache Maven Wagon</a>.
+ */
+package org.eclipse.aether.transport.wagon;
+
diff --git a/maven-resolver-transport-wagon/src/site/site.xml b/maven-resolver-transport-wagon/src/site/site.xml
new file mode 100644
index 0000000..ffa91f4
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Transport Wagon">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/AbstractWagonTransporterTest.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/AbstractWagonTransporterTest.java
new file mode 100644
index 0000000..adf080e
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/AbstractWagonTransporterTest.java
@@ -0,0 +1,535 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.Wagon;
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.transport.GetTask;
+import org.eclipse.aether.spi.connector.transport.PeekTask;
+import org.eclipse.aether.spi.connector.transport.PutTask;
+import org.eclipse.aether.spi.connector.transport.Transporter;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.NoTransporterException;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public abstract class AbstractWagonTransporterTest
+{
+
+    private DefaultRepositorySystemSession session;
+
+    private TransporterFactory factory;
+
+    private Transporter transporter;
+
+    private String id;
+
+    private Map<String, String> fs;
+
+    protected abstract Wagon newWagon();
+
+    private RemoteRepository newRepo( String url )
+    {
+        return new RemoteRepository.Builder( "test", "default", url ).build();
+    }
+
+    private void newTransporter( String url )
+        throws Exception
+    {
+        newTransporter( newRepo( url ) );
+    }
+
+    private void newTransporter( RemoteRepository repo )
+        throws Exception
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        transporter = factory.newInstance( session, repo );
+    }
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        session = TestUtils.newSession();
+        factory = new WagonTransporterFactory( new WagonProvider()
+        {
+            public Wagon lookup( String roleHint )
+                throws Exception
+            {
+                if ( "mem".equalsIgnoreCase( roleHint ) )
+                {
+                    return newWagon();
+                }
+                throw new IllegalArgumentException( "unknown wagon role: " + roleHint );
+            }
+
+            public void release( Wagon wagon )
+            {
+            }
+        }, new WagonConfigurator()
+        {
+            public void configure( Wagon wagon, Object configuration )
+                throws Exception
+            {
+                ( (Configurable) wagon ).setConfiguration( configuration );
+            }
+        }, new TestLoggerFactory() );
+        id = UUID.randomUUID().toString().replace( "-", "" );
+        fs = MemWagonUtils.getFilesystem( id );
+        fs.put( "file.txt", "test" );
+        fs.put( "empty.txt", "" );
+        fs.put( "some space.txt", "space" );
+        newTransporter( "mem://" + id );
+    }
+
+    @After
+    public void tearDown()
+    {
+        if ( transporter != null )
+        {
+            transporter.close();
+            transporter = null;
+        }
+        factory = null;
+        session = null;
+    }
+
+    @Test
+    public void testClassify()
+    {
+        assertEquals( Transporter.ERROR_OTHER, transporter.classify( new TransferFailedException( "test" ) ) );
+        assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( new ResourceDoesNotExistException( "test" ) ) );
+    }
+
+    @Test
+    public void testPeek()
+        throws Exception
+    {
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testPeek_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceDoesNotExistException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPeek_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.peek( new PeekTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_ToMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", task.getDataString() );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( task.getDataString(), new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_ToFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "test", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "test", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EmptyResource()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "failure" );
+        assertTrue( file.delete() && !file.exists() );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        GetTask task = new GetTask( URI.create( "empty.txt" ) ).setDataFile( file ).setListener( listener );
+        transporter.get( task );
+        assertEquals( "", TestFileUtils.readString( file ) );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", new String( listener.baos.toByteArray(), StandardCharsets.UTF_8 ) );
+    }
+
+    @Test
+    public void testGet_EncodedResourcePath()
+        throws Exception
+    {
+        GetTask task = new GetTask( URI.create( "some%20space.txt" ) );
+        transporter.get( task );
+        assertEquals( "space", task.getDataString() );
+    }
+
+    @Test
+    public void testGet_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File file = TestFileUtils.createTempFile( "failure" );
+            transporter.get( new GetTask( URI.create( "file.txt" ) ).setDataFile( file ) );
+            assertTrue( i + ", " + file.getAbsolutePath(), file.delete() );
+        }
+    }
+
+    @Test
+    public void testGet_NotFound()
+        throws Exception
+    {
+        try
+        {
+            transporter.get( new GetTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( ResourceDoesNotExistException e )
+        {
+            assertEquals( Transporter.ERROR_NOT_FOUND, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.get( new GetTask( URI.create( "file.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testGet_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.get( task );
+        assertEquals( 1, listener.startedCount );
+    }
+
+    @Test
+    public void testGet_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        GetTask task = new GetTask( URI.create( "file.txt" ) ).setListener( listener );
+        try
+        {
+            transporter.get( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 4L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test
+    public void testPut_FromMemory()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", fs.get( "file.txt" ) );
+    }
+
+    @Test
+    public void testPut_FromFile()
+        throws Exception
+    {
+        File file = TestFileUtils.createTempFile( "upload" );
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataFile( file );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", fs.get( "file.txt" ) );
+    }
+
+    @Test
+    public void testPut_EmptyResource()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 0L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 0, listener.progressedCount );
+        assertEquals( "", fs.get( "file.txt" ) );
+    }
+
+    @Test
+    public void testPut_NonExistentParentDir()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task =
+            new PutTask( URI.create( "dir/sub/dir/file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "upload", fs.get( "dir/sub/dir/file.txt" ) );
+    }
+
+    @Test
+    public void testPut_EncodedResourcePath()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        PutTask task = new PutTask( URI.create( "some%20space.txt" ) ).setListener( listener ).setDataString( "OK" );
+        transporter.put( task );
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 2L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertTrue( "Count: " + listener.progressedCount, listener.progressedCount > 0 );
+        assertEquals( "OK", fs.get( "some space.txt" ) );
+    }
+
+    @Test
+    public void testPut_FileHandleLeak()
+        throws Exception
+    {
+        for ( int i = 0; i < 100; i++ )
+        {
+            File src = TestFileUtils.createTempFile( "upload" );
+            transporter.put( new PutTask( URI.create( "file.txt" ) ).setDataFile( src ) );
+            assertTrue( i + ", " + src.getAbsolutePath(), src.delete() );
+        }
+    }
+
+    @Test
+    public void testPut_Closed()
+        throws Exception
+    {
+        transporter.close();
+        try
+        {
+            transporter.put( new PutTask( URI.create( "missing.txt" ) ) );
+            fail( "Expected error" );
+        }
+        catch ( IllegalStateException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+    }
+
+    @Test
+    public void testPut_StartCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelStart = true;
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        transporter.put( task );
+        assertEquals( 1, listener.startedCount );
+    }
+
+    @Test
+    public void testPut_ProgressCancelled()
+        throws Exception
+    {
+        RecordingTransportListener listener = new RecordingTransportListener();
+        listener.cancelProgress = true;
+        PutTask task = new PutTask( URI.create( "file.txt" ) ).setListener( listener ).setDataString( "upload" );
+        try
+        {
+            transporter.put( task );
+            fail( "Expected error" );
+        }
+        catch ( TransferCancelledException e )
+        {
+            assertEquals( Transporter.ERROR_OTHER, transporter.classify( e ) );
+        }
+        assertEquals( 0L, listener.dataOffset );
+        assertEquals( 6L, listener.dataLength );
+        assertEquals( 1, listener.startedCount );
+        assertEquals( 1, listener.progressedCount );
+    }
+
+    @Test( expected = NoTransporterException.class )
+    public void testInit_BadProtocol()
+        throws Exception
+    {
+        newTransporter( "bad:/void" );
+    }
+
+    @Test
+    public void testInit_CaseInsensitiveProtocol()
+        throws Exception
+    {
+        newTransporter( "mem:/void" );
+        newTransporter( "MEM:/void" );
+        newTransporter( "mEm:/void" );
+    }
+
+    @Test
+    public void testInit_Configuration()
+        throws Exception
+    {
+        session.setConfigProperty( "aether.connector.wagon.config.test", "passed" );
+        newTransporter( "mem://" + id + "?config=passed" );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testInit_UserAgent()
+        throws Exception
+    {
+        session.setConfigProperty( ConfigurationProperties.USER_AGENT, "Test/1.0" );
+        newTransporter( "mem://" + id + "?userAgent=Test/1.0" );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testInit_Timeout()
+        throws Exception
+    {
+        session.setConfigProperty( ConfigurationProperties.REQUEST_TIMEOUT, "12345678" );
+        newTransporter( "mem://" + id + "?requestTimeout=12345678" );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testInit_ServerAuth()
+        throws Exception
+    {
+        String url =
+            "mem://" + id + "?serverUsername=testuser&serverPassword=testpass"
+                + "&serverPrivateKey=testkey&serverPassphrase=testphrase";
+        Authentication auth =
+            new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).addPrivateKey( "testkey",
+                                                                                                           "testphrase" ).build();
+        RemoteRepository repo =
+            new RemoteRepository.Builder( "test", "default", url ).setAuthentication( auth ).build();
+        newTransporter( repo );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testInit_Proxy()
+        throws Exception
+    {
+        String url = "mem://" + id + "?proxyHost=testhost&proxyPort=8888";
+        RemoteRepository repo =
+            new RemoteRepository.Builder( "test", "default", url ).setProxy( new Proxy( "http", "testhost", 8888 ) ).build();
+        newTransporter( repo );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+    @Test
+    public void testInit_ProxyAuth()
+        throws Exception
+    {
+        String url = "mem://" + id + "?proxyUsername=testuser&proxyPassword=testpass";
+        Authentication auth = new AuthenticationBuilder().addUsername( "testuser" ).addPassword( "testpass" ).build();
+        RemoteRepository repo =
+            new RemoteRepository.Builder( "test", "default", url ).setProxy( new Proxy( "http", "testhost", 8888, auth ) ).build();
+        newTransporter( repo );
+        transporter.peek( new PeekTask( URI.create( "file.txt" ) ) );
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/Configurable.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/Configurable.java
new file mode 100644
index 0000000..9a90f6f
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/Configurable.java
@@ -0,0 +1,31 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+/**
+ */
+public interface Configurable
+{
+
+    Object getConfiguration();
+
+    void setConfiguration( Object config );
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemStreamWagon.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemStreamWagon.java
new file mode 100644
index 0000000..6b4be8f
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemStreamWagon.java
@@ -0,0 +1,127 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.maven.wagon.ConnectionException;
+import org.apache.maven.wagon.InputData;
+import org.apache.maven.wagon.OutputData;
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.StreamWagon;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.authentication.AuthenticationException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
+import org.apache.maven.wagon.resource.Resource;
+
+/**
+ */
+public class MemStreamWagon
+    extends StreamWagon
+    implements Configurable
+{
+
+    private Map<String, String> fs;
+
+    private Properties headers;
+
+    private Object config;
+
+    public void setConfiguration( Object config )
+    {
+        this.config = config;
+    }
+
+    public Object getConfiguration()
+    {
+        return config;
+    }
+
+    public void setHttpHeaders( Properties httpHeaders )
+    {
+        headers = httpHeaders;
+    }
+
+    @Override
+    protected void openConnectionInternal()
+        throws ConnectionException, AuthenticationException
+    {
+        fs =
+            MemWagonUtils.openConnection( this, getAuthenticationInfo(),
+                                          getProxyInfo( "mem", getRepository().getHost() ), headers );
+    }
+
+    @Override
+    public void closeConnection()
+        throws ConnectionException
+    {
+        fs = null;
+    }
+
+    private String getData( String resource )
+    {
+        return fs.get( URI.create( resource ).getSchemeSpecificPart() );
+    }
+
+    @Override
+    public boolean resourceExists( String resourceName )
+        throws TransferFailedException, AuthorizationException
+    {
+        String data = getData( resourceName );
+        return data != null;
+    }
+
+    @Override
+    public void fillInputData( InputData inputData )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        String data = getData( inputData.getResource().getName() );
+        if ( data == null )
+        {
+            throw new ResourceDoesNotExistException( "Missing resource: " + inputData.getResource().getName() );
+        }
+        byte[] bytes = data.getBytes( StandardCharsets.UTF_8 );
+        inputData.getResource().setContentLength( bytes.length );
+        inputData.setInputStream( new ByteArrayInputStream( bytes ) );
+    }
+
+    @Override
+    public void fillOutputData( OutputData outputData )
+        throws TransferFailedException
+    {
+        outputData.setOutputStream( new ByteArrayOutputStream() );
+    }
+
+    @Override
+    protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
+        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
+    {
+        String data = new String( ( (ByteArrayOutputStream) output ).toByteArray(), StandardCharsets.UTF_8 );
+        fs.put( URI.create( resource.getName() ).getSchemeSpecificPart(), data );
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagon.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagon.java
new file mode 100644
index 0000000..17c82f2
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagon.java
@@ -0,0 +1,216 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.maven.wagon.AbstractWagon;
+import org.apache.maven.wagon.ConnectionException;
+import org.apache.maven.wagon.InputData;
+import org.apache.maven.wagon.OutputData;
+import org.apache.maven.wagon.ResourceDoesNotExistException;
+import org.apache.maven.wagon.TransferFailedException;
+import org.apache.maven.wagon.authentication.AuthenticationException;
+import org.apache.maven.wagon.authorization.AuthorizationException;
+import org.apache.maven.wagon.events.TransferEvent;
+import org.apache.maven.wagon.resource.Resource;
+
+/**
+ */
+public class MemWagon
+    extends AbstractWagon
+    implements Configurable
+{
+
+    private Map<String, String> fs;
+
+    private Properties headers;
+
+    private Object config;
+
+    public void setConfiguration( Object config )
+    {
+        this.config = config;
+    }
+
+    public Object getConfiguration()
+    {
+        return config;
+    }
+
+    public void setHttpHeaders( Properties httpHeaders )
+    {
+        headers = httpHeaders;
+    }
+
+    @Override
+    protected void openConnectionInternal()
+        throws ConnectionException, AuthenticationException
+    {
+        fs =
+            MemWagonUtils.openConnection( this, getAuthenticationInfo(),
+                                          getProxyInfo( "mem", getRepository().getHost() ), headers );
+    }
+
+    @Override
+    protected void closeConnection()
+        throws ConnectionException
+    {
+        fs = null;
+    }
+
+    private String getData( String resource )
+    {
+        return fs.get( URI.create( resource ).getSchemeSpecificPart() );
+    }
+
+    @Override
+    public boolean resourceExists( String resourceName )
+        throws TransferFailedException, AuthorizationException
+    {
+        String data = getData( resourceName );
+        return data != null;
+    }
+
+    public void get( String resourceName, File destination )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        getIfNewer( resourceName, destination, 0 );
+    }
+
+    public boolean getIfNewer( String resourceName, File destination, long timestamp )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        Resource resource = new Resource( resourceName );
+        fireGetInitiated( resource, destination );
+        resource.setLastModified( timestamp );
+        getTransfer( resource, destination, getInputStream( resource ) );
+        return true;
+    }
+
+    protected InputStream getInputStream( Resource resource )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        InputData inputData = new InputData();
+        inputData.setResource( resource );
+        try
+        {
+            fillInputData( inputData );
+        }
+        catch ( TransferFailedException e )
+        {
+            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
+            cleanupGetTransfer( resource );
+            throw e;
+        }
+        catch ( ResourceDoesNotExistException e )
+        {
+            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
+            cleanupGetTransfer( resource );
+            throw e;
+        }
+        catch ( AuthorizationException e )
+        {
+            fireTransferError( resource, e, TransferEvent.REQUEST_GET );
+            cleanupGetTransfer( resource );
+            throw e;
+        }
+        finally
+        {
+            if ( inputData.getInputStream() == null )
+            {
+                cleanupGetTransfer( resource );
+            }
+        }
+        return inputData.getInputStream();
+    }
+
+    protected void fillInputData( InputData inputData )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        String data = getData( inputData.getResource().getName() );
+        if ( data == null )
+        {
+            throw new ResourceDoesNotExistException( "Missing resource: " + inputData.getResource().getName() );
+        }
+        byte[] bytes = data.getBytes( StandardCharsets.UTF_8 );
+        inputData.getResource().setContentLength( bytes.length );
+        inputData.setInputStream( new ByteArrayInputStream( bytes ) );
+    }
+
+    public void put( File source, String resourceName )
+        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
+    {
+        Resource resource = new Resource( resourceName );
+        firePutInitiated( resource, source );
+        resource.setContentLength( source.length() );
+        resource.setLastModified( source.lastModified() );
+        OutputStream os = getOutputStream( resource );
+        putTransfer( resource, source, os, true );
+    }
+
+    protected OutputStream getOutputStream( Resource resource )
+        throws TransferFailedException
+    {
+        OutputData outputData = new OutputData();
+        outputData.setResource( resource );
+        try
+        {
+            fillOutputData( outputData );
+        }
+        catch ( TransferFailedException e )
+        {
+            fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
+            throw e;
+        }
+        finally
+        {
+            if ( outputData.getOutputStream() == null )
+            {
+                cleanupPutTransfer( resource );
+            }
+        }
+
+        return outputData.getOutputStream();
+    }
+
+    protected void fillOutputData( OutputData outputData )
+        throws TransferFailedException
+    {
+        outputData.setOutputStream( new ByteArrayOutputStream() );
+    }
+
+    @Override
+    protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
+        throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException
+    {
+        String data = new String( ( (ByteArrayOutputStream) output ).toByteArray(), StandardCharsets.UTF_8 );
+        fs.put( URI.create( resource.getName() ).getSchemeSpecificPart(), data );
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagonUtils.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagonUtils.java
new file mode 100644
index 0000000..86d2cc7
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/MemWagonUtils.java
@@ -0,0 +1,113 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.maven.wagon.ConnectionException;
+import org.apache.maven.wagon.Wagon;
+import org.apache.maven.wagon.authentication.AuthenticationException;
+import org.apache.maven.wagon.authentication.AuthenticationInfo;
+import org.apache.maven.wagon.proxy.ProxyInfo;
+
+/**
+ */
+class MemWagonUtils
+{
+
+    private static final ConcurrentMap<String, Map<String, String>> mounts =
+        new ConcurrentHashMap<String, Map<String, String>>();
+
+    public static Map<String, String> getFilesystem( String id )
+    {
+        Map<String, String> fs = mounts.get( id );
+        if ( fs == null )
+        {
+            fs = new ConcurrentHashMap<String, String>();
+            Map<String, String> prev = mounts.putIfAbsent( id, fs );
+            if ( prev != null )
+            {
+                fs = prev;
+            }
+        }
+        return fs;
+    }
+
+    public static Map<String, String> openConnection( Wagon wagon, AuthenticationInfo auth, ProxyInfo proxy,
+                                                      Properties headers )
+        throws ConnectionException, AuthenticationException
+    {
+        URI uri = URI.create( wagon.getRepository().getUrl() );
+
+        String query = uri.getQuery();
+        if ( query != null )
+        {
+            verify( query, "config", String.valueOf( ( (Configurable) wagon ).getConfiguration() ) );
+
+            verify( query, "userAgent", ( headers != null ) ? headers.getProperty( "User-Agent" ) : null );
+            verify( query, "requestTimeout", Integer.toString( wagon.getTimeout() ) );
+
+            verify( query, "serverUsername", ( auth != null ) ? auth.getUserName() : null );
+            verify( query, "serverPassword", ( auth != null ) ? auth.getPassword() : null );
+            verify( query, "serverPrivateKey", ( auth != null ) ? auth.getPrivateKey() : null );
+            verify( query, "serverPassphrase", ( auth != null ) ? auth.getPassphrase() : null );
+
+            verify( query, "proxyHost", ( proxy != null ) ? proxy.getHost() : null );
+            verify( query, "proxyPort", ( proxy != null ) ? Integer.toString( proxy.getPort() ) : null );
+            verify( query, "proxyUsername", ( proxy != null ) ? proxy.getUserName() : null );
+            verify( query, "proxyPassword", ( proxy != null ) ? proxy.getPassword() : null );
+        }
+
+        return getFilesystem( uri.getHost() );
+    }
+
+    private static void verify( String query, String key, String value )
+        throws ConnectionException
+    {
+        int index = query.indexOf( key + "=" );
+        if ( index < 0 )
+        {
+            return;
+        }
+        String expected = query.substring( index + key.length() + 1 );
+        index = expected.indexOf( "&" );
+        if ( index >= 0 )
+        {
+            expected = expected.substring( 0, index );
+        }
+
+        if ( expected.length() == 0 )
+        {
+            if ( value != null )
+            {
+                throw new ConnectionException( "Bad " + key + ": " + value );
+            }
+        }
+        else if ( !expected.equals( value ) )
+        {
+            throw new ConnectionException( "Bad " + key + ": " + value );
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/PlexusSupportTest.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/PlexusSupportTest.java
new file mode 100644
index 0000000..231fa95
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/PlexusSupportTest.java
@@ -0,0 +1,50 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.codehaus.plexus.ContainerConfiguration;
+import org.codehaus.plexus.PlexusTestCase;
+import org.eclipse.aether.internal.test.util.TestLoggerFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.log.LoggerFactory;
+
+/**
+ */
+public class PlexusSupportTest
+    extends PlexusTestCase
+{
+
+    @Override
+    protected void customizeContainerConfiguration( ContainerConfiguration containerConfiguration )
+    {
+        containerConfiguration.setClassPathScanning( "cache" );
+    }
+
+    public void testExistenceOfPlexusComponentMetadata()
+        throws Exception
+    {
+        getContainer().addComponent( new TestLoggerFactory(), LoggerFactory.class, null );
+
+        TransporterFactory factory = lookup( TransporterFactory.class, "wagon" );
+        assertNotNull( factory );
+        assertEquals( WagonTransporterFactory.class, factory.getClass() );
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/RecordingTransportListener.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/RecordingTransportListener.java
new file mode 100644
index 0000000..7f61985
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/RecordingTransportListener.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+import org.eclipse.aether.spi.connector.transport.TransportListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+
+class RecordingTransportListener
+    extends TransportListener
+{
+
+    public final ByteArrayOutputStream baos = new ByteArrayOutputStream( 1024 );
+
+    public long dataOffset;
+
+    public long dataLength;
+
+    public int startedCount;
+
+    public int progressedCount;
+
+    public boolean cancelStart;
+
+    public boolean cancelProgress;
+
+    @Override
+    public void transportStarted( long dataOffset, long dataLength )
+        throws TransferCancelledException
+    {
+        startedCount++;
+        progressedCount = 0;
+        this.dataLength = dataLength;
+        this.dataOffset = dataOffset;
+        baos.reset();
+        if ( cancelStart )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+    @Override
+    public void transportProgressed( ByteBuffer data )
+        throws TransferCancelledException
+    {
+        progressedCount++;
+        baos.write( data.array(), data.arrayOffset() + data.position(), data.remaining() );
+        if ( cancelProgress )
+        {
+            throw new TransferCancelledException();
+        }
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/StreamWagonTransporterTest.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/StreamWagonTransporterTest.java
new file mode 100644
index 0000000..c3f3fd4
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/StreamWagonTransporterTest.java
@@ -0,0 +1,36 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.apache.maven.wagon.Wagon;
+
+/**
+ */
+public class StreamWagonTransporterTest
+    extends AbstractWagonTransporterTest
+{
+
+    @Override
+    protected Wagon newWagon()
+    {
+        return new MemStreamWagon();
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/WagonTransporterTest.java b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/WagonTransporterTest.java
new file mode 100644
index 0000000..5a10399
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/java/org/eclipse/aether/transport/wagon/WagonTransporterTest.java
@@ -0,0 +1,36 @@
+package org.eclipse.aether.transport.wagon;
+
+/*
+ * 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.
+ */
+
+import org.apache.maven.wagon.Wagon;
+
+/**
+ */
+public class WagonTransporterTest
+    extends AbstractWagonTransporterTest
+{
+
+    @Override
+    protected Wagon newWagon()
+    {
+        return new MemWagon();
+    }
+
+}
diff --git a/maven-resolver-transport-wagon/src/test/resources/logback-test.xml b/maven-resolver-transport-wagon/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..d031998
--- /dev/null
+++ b/maven-resolver-transport-wagon/src/test/resources/logback-test.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ ! 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.
+ !-->
+
+<configuration>
+  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{HH:mm:ss.SSS} [%-18thread] %c{1} [%p] %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <logger name="org.sonatype.tests.jetty.server" level="INFO"/>
+  <logger name="org.sonatype.tests.jetty.server.behaviour" level="DEBUG"/>
+  <logger name="org.sonatype.tests" level="DEBUG"/>
+
+  <root level="WARN">
+    <appender-ref ref="CONSOLE"/>
+  </root>
+</configuration>
diff --git a/maven-resolver-util/pom.xml b/maven-resolver-util/pom.xml
new file mode 100644
index 0000000..87c6a6c
--- /dev/null
+++ b/maven-resolver-util/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.resolver</groupId>
+    <artifactId>maven-resolver</artifactId>
+    <version>1.1.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-resolver-util</artifactId>
+
+  <name>Maven Artifact Resolver Utilities</name>
+  <description>
+    A collection of utility classes to ease usage of the repository system.
+  </description>
+
+  <properties>
+    <AutomaticModuleName>org.apache.maven.resolver.util</AutomaticModuleName>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.resolver</groupId>
+      <artifactId>maven-resolver-test-util</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
new file mode 100644
index 0000000..415e712
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
@@ -0,0 +1,211 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A utility class to assist in the verification and generation of checksums.
+ */
+public final class ChecksumUtils
+{
+
+    private ChecksumUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Extracts the checksum from the specified file.
+     * 
+     * @param checksumFile The path to the checksum file, must not be {@code null}.
+     * @return The checksum stored in the file, never {@code null}.
+     * @throws IOException If the checksum does not exist or could not be read for other reasons.
+     */
+    public static String read( File checksumFile )
+        throws IOException
+    {
+        String checksum = "";
+        BufferedReader br = null;
+        try
+        {
+            br = new BufferedReader( new InputStreamReader( new FileInputStream( checksumFile ), StandardCharsets.UTF_8 ), 512 );
+            while ( true )
+            {
+                String line = br.readLine();
+                if ( line == null )
+                {
+                    break;
+                }
+                line = line.trim();
+                if ( line.length() > 0 )
+                {
+                    checksum = line;
+                    break;
+                }
+            }
+        }
+        finally
+        {
+            try
+            {
+                if ( br != null )
+                {
+                    br.close();
+                    br = null;
+                }
+            }
+            catch ( IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+
+        if ( checksum.matches( ".+= [0-9A-Fa-f]+" ) )
+        {
+            int lastSpacePos = checksum.lastIndexOf( ' ' );
+            checksum = checksum.substring( lastSpacePos + 1 );
+        }
+        else
+        {
+            int spacePos = checksum.indexOf( ' ' );
+
+            if ( spacePos != -1 )
+            {
+                checksum = checksum.substring( 0, spacePos );
+            }
+        }
+
+        return checksum;
+    }
+
+    /**
+     * Calculates checksums for the specified file.
+     * 
+     * @param dataFile The file for which to calculate checksums, must not be {@code null}.
+     * @param algos The names of checksum algorithms (cf. {@link MessageDigest#getInstance(String)} to use, must not be
+     *            {@code null}.
+     * @return The calculated checksums, indexed by algorithm name, or the exception that occurred while trying to
+     *         calculate it, never {@code null}.
+     * @throws IOException If the data file could not be read.
+     */
+    public static Map<String, Object> calc( File dataFile, Collection<String> algos )
+        throws IOException
+    {
+        Map<String, Object> results = new LinkedHashMap<String, Object>();
+
+        Map<String, MessageDigest> digests = new LinkedHashMap<String, MessageDigest>();
+        for ( String algo : algos )
+        {
+            try
+            {
+                digests.put( algo, MessageDigest.getInstance( algo ) );
+            }
+            catch ( NoSuchAlgorithmException e )
+            {
+                results.put( algo, e );
+            }
+        }
+
+        InputStream in = null;
+        try
+        {
+            in = new FileInputStream( dataFile );
+            for ( byte[] buffer = new byte[ 32 * 1024 ];; )
+            {
+                int read = in.read( buffer );
+                if ( read < 0 )
+                {
+                    break;
+                }
+                for ( MessageDigest digest : digests.values() )
+                {
+                    digest.update( buffer, 0, read );
+                }
+            }
+            in.close();
+            in = null;
+        }
+        finally
+        {
+            try
+            {
+                if ( in != null )
+                {
+                    in.close();
+                }
+            }
+            catch ( IOException e )
+            {
+                // Suppressed due to an exception already thrown in the try block.
+            }
+        }
+
+        for ( Map.Entry<String, MessageDigest> entry : digests.entrySet() )
+        {
+            byte[] bytes = entry.getValue().digest();
+
+            results.put( entry.getKey(), toHexString( bytes ) );
+        }
+
+        return results;
+    }
+
+    /**
+     * Creates a hexadecimal representation of the specified bytes. Each byte is converted into a two-digit hex number
+     * and appended to the result with no separator between consecutive bytes.
+     * 
+     * @param bytes The bytes to represent in hex notation, may be be {@code null}.
+     * @return The hexadecimal representation of the input or {@code null} if the input was {@code null}.
+     */
+    public static String toHexString( byte[] bytes )
+    {
+        if ( bytes == null )
+        {
+            return null;
+        }
+
+        StringBuilder buffer = new StringBuilder( bytes.length * 2 );
+
+        for ( byte aByte : bytes )
+        {
+            int b = aByte & 0xFF;
+            if ( b < 0x10 )
+            {
+                buffer.append( '0' );
+            }
+            buffer.append( Integer.toHexString( b ) );
+        }
+
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
new file mode 100644
index 0000000..2f53856
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ConfigUtils.java
@@ -0,0 +1,392 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * A utility class to read configuration properties from a repository system session.
+ * 
+ * @see RepositorySystemSession#getConfigProperties()
+ */
+public final class ConfigUtils
+{
+
+    private ConfigUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys are set, may be {@code null}.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a valid value is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static Object getObject( Map<?, ?> properties, Object defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value != null )
+            {
+                return value;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys are set, may be {@code null}.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a valid value is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static Object getObject( RepositorySystemSession session, Object defaultValue, String... keys )
+    {
+        return getObject( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a string, may be
+     *            {@code null}.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a string value is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static String getString( Map<?, ?> properties, String defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof String )
+            {
+                return (String) value;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a string, may be
+     *            {@code null}.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a string value is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static String getString( RepositorySystemSession session, String defaultValue, String... keys )
+    {
+        return getString( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of an {@link Integer} is found.
+     * @return The property value.
+     */
+    public static int getInteger( Map<?, ?> properties, int defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof Number )
+            {
+                return ( (Number) value ).intValue();
+            }
+
+            try
+            {
+                return Integer.valueOf( (String) value );
+            }
+            catch ( Exception e )
+            {
+                // try next key
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of an {@link Integer} is found.
+     * @return The property value.
+     */
+    public static int getInteger( RepositorySystemSession session, int defaultValue, String... keys )
+    {
+        return getInteger( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of a {@link Long} is found.
+     * @return The property value.
+     */
+    public static long getLong( Map<?, ?> properties, long defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof Number )
+            {
+                return ( (Number) value ).longValue();
+            }
+
+            try
+            {
+                return Long.valueOf( (String) value );
+            }
+            catch ( Exception e )
+            {
+                // try next key
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of a {@link Long} is found.
+     * @return The property value.
+     */
+    public static long getLong( RepositorySystemSession session, long defaultValue, String... keys )
+    {
+        return getLong( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of a {@link Float} is found.
+     * @return The property value.
+     */
+    public static float getFloat( Map<?, ?> properties, float defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof Number )
+            {
+                return ( (Number) value ).floatValue();
+            }
+
+            try
+            {
+                return Float.valueOf( (String) value );
+            }
+            catch ( Exception e )
+            {
+                // try next key
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a number.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Number} or a string representation of a {@link Float} is found.
+     * @return The property value.
+     */
+    public static float getFloat( RepositorySystemSession session, float defaultValue, String... keys )
+    {
+        return getFloat( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a boolean.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Boolean} or a string (to be {@link Boolean#parseBoolean(String) parsed as boolean}) is found.
+     * @return The property value.
+     */
+    public static boolean getBoolean( Map<?, ?> properties, boolean defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof Boolean )
+            {
+                return (Boolean) value;
+            }
+            else if ( value instanceof String )
+            {
+                return Boolean.parseBoolean( (String) value );
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a boolean.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a {@link Boolean} or a string (to be {@link Boolean#parseBoolean(String) parsed as boolean}) is found.
+     * @return The property value.
+     */
+    public static boolean getBoolean( RepositorySystemSession session, boolean defaultValue, String... keys )
+    {
+        return getBoolean( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a collection.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a collection is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static List<?> getList( Map<?, ?> properties, List<?> defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof List )
+            {
+                return (List<?>) value;
+            }
+            else if ( value instanceof Collection )
+            {
+                return Collections.unmodifiableList( new ArrayList<Object>( (Collection<?>) value ) );
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a collection.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a collection is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static List<?> getList( RepositorySystemSession session, List<?> defaultValue, String... keys )
+    {
+        return getList( session.getConfigProperties(), defaultValue, keys );
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param properties The configuration properties to read, must not be {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a map.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a map is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static Map<?, ?> getMap( Map<?, ?> properties, Map<?, ?> defaultValue, String... keys )
+    {
+        for ( String key : keys )
+        {
+            Object value = properties.get( key );
+
+            if ( value instanceof Map )
+            {
+                return (Map<?, ?>) value;
+            }
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets the specified configuration property.
+     * 
+     * @param session The repository system session from which to read the configuration property, must not be
+     *            {@code null}.
+     * @param defaultValue The default value to return in case none of the property keys is set to a map.
+     * @param keys The property keys to read, must not be {@code null}. The specified keys are read one after one until
+     *            a map is found.
+     * @return The property value or {@code null} if none.
+     */
+    public static Map<?, ?> getMap( RepositorySystemSession session, Map<?, ?> defaultValue, String... keys )
+    {
+        return getMap( session.getConfigProperties(), defaultValue, keys );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringUtils.java
new file mode 100644
index 0000000..e0ed12a
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringUtils.java
@@ -0,0 +1,44 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+/**
+ * A utility class to ease string processing.
+ */
+public final class StringUtils
+{
+
+    private StringUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Checks whether a string is {@code null} or of zero length.
+     * 
+     * @param string The string to check, may be {@code null}.
+     * @return {@code true} if the string is {@code null} or of zero length, {@code false} otherwise.
+     */
+    public static boolean isEmpty( String string )
+    {
+        return string == null || string.length() <= 0;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/ArtifactIdUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/ArtifactIdUtils.java
new file mode 100644
index 0000000..54ffc64
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/ArtifactIdUtils.java
@@ -0,0 +1,269 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * A utility class for artifact identifiers.
+ */
+public final class ArtifactIdUtils
+{
+
+    private static final char SEP = ':';
+
+    private ArtifactIdUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Creates an artifact identifier of the form {@code <groupId>:<artifactId>:<extension>[:<classifier>]:<version>}.
+     * 
+     * @param artifact The artifact to create an identifer for, may be {@code null}.
+     * @return The artifact identifier or {@code null} if the input was {@code null}.
+     */
+    public static String toId( Artifact artifact )
+    {
+        String id = null;
+        if ( artifact != null )
+        {
+            id =
+                toId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
+                      artifact.getClassifier(), artifact.getVersion() );
+        }
+        return id;
+    }
+
+    /**
+     * Creates an artifact identifier of the form {@code <groupId>:<artifactId>:<extension>[:<classifier>]:<version>}.
+     * 
+     * @param groupId The group id, may be {@code null}.
+     * @param artifactId The artifact id, may be {@code null}.
+     * @param extension The file extensiion, may be {@code null}.
+     * @param classifier The classifier, may be {@code null}.
+     * @param version The version, may be {@code null}.
+     * @return The artifact identifier, never {@code null}.
+     */
+    public static String toId( String groupId, String artifactId, String extension, String classifier, String version )
+    {
+        StringBuilder buffer = concat( groupId, artifactId, extension, classifier );
+        buffer.append( SEP );
+        if ( version != null )
+        {
+            buffer.append( version );
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Creates an artifact identifier of the form
+     * {@code <groupId>:<artifactId>:<extension>[:<classifier>]:<baseVersion>}.
+     * 
+     * @param artifact The artifact to create an identifer for, may be {@code null}.
+     * @return The artifact identifier or {@code null} if the input was {@code null}.
+     */
+    public static String toBaseId( Artifact artifact )
+    {
+        String id = null;
+        if ( artifact != null )
+        {
+            id =
+                toId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
+                      artifact.getClassifier(), artifact.getBaseVersion() );
+        }
+        return id;
+    }
+
+    /**
+     * Creates an artifact identifier of the form {@code <groupId>:<artifactId>:<extension>[:<classifier>]}.
+     * 
+     * @param artifact The artifact to create an identifer for, may be {@code null}.
+     * @return The artifact identifier or {@code null} if the input was {@code null}.
+     */
+    public static String toVersionlessId( Artifact artifact )
+    {
+        String id = null;
+        if ( artifact != null )
+        {
+            id =
+                toVersionlessId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
+                                 artifact.getClassifier() );
+        }
+        return id;
+    }
+
+    /**
+     * Creates an artifact identifier of the form {@code <groupId>:<artifactId>:<extension>[:<classifier>]}.
+     * 
+     * @param groupId The group id, may be {@code null}.
+     * @param artifactId The artifact id, may be {@code null}.
+     * @param extension The file extensiion, may be {@code null}.
+     * @param classifier The classifier, may be {@code null}.
+     * @return The artifact identifier, never {@code null}.
+     */
+    public static String toVersionlessId( String groupId, String artifactId, String extension, String classifier )
+    {
+        return concat( groupId, artifactId, extension, classifier ).toString();
+    }
+
+    private static StringBuilder concat( String groupId, String artifactId, String extension, String classifier )
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+
+        if ( groupId != null )
+        {
+            buffer.append( groupId );
+        }
+        buffer.append( SEP );
+        if ( artifactId != null )
+        {
+            buffer.append( artifactId );
+        }
+        buffer.append( SEP );
+        if ( extension != null )
+        {
+            buffer.append( extension );
+        }
+        if ( classifier != null && classifier.length() > 0 )
+        {
+            buffer.append( SEP ).append( classifier );
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Determines whether two artifacts have the same identifier. This method is equivalent to calling
+     * {@link String#equals(Object)} on the return values from {@link #toId(Artifact)} for the artifacts but does not
+     * incur the overhead of creating temporary strings.
+     * 
+     * @param artifact1 The first artifact, may be {@code null}.
+     * @param artifact2 The second artifact, may be {@code null}.
+     * @return {@code true} if both artifacts are not {@code null} and have equal ids, {@code false} otherwise.
+     */
+    public static boolean equalsId( Artifact artifact1, Artifact artifact2 )
+    {
+        if ( artifact1 == null || artifact2 == null )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getArtifactId(), artifact2.getArtifactId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getGroupId(), artifact2.getGroupId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getExtension(), artifact2.getExtension() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getClassifier(), artifact2.getClassifier() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getVersion(), artifact2.getVersion() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Determines whether two artifacts have the same base identifier. This method is equivalent to calling
+     * {@link String#equals(Object)} on the return values from {@link #toBaseId(Artifact)} for the artifacts but does
+     * not incur the overhead of creating temporary strings.
+     * 
+     * @param artifact1 The first artifact, may be {@code null}.
+     * @param artifact2 The second artifact, may be {@code null}.
+     * @return {@code true} if both artifacts are not {@code null} and have equal base ids, {@code false} otherwise.
+     */
+    public static boolean equalsBaseId( Artifact artifact1, Artifact artifact2 )
+    {
+        if ( artifact1 == null || artifact2 == null )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getArtifactId(), artifact2.getArtifactId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getGroupId(), artifact2.getGroupId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getExtension(), artifact2.getExtension() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getClassifier(), artifact2.getClassifier() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getBaseVersion(), artifact2.getBaseVersion() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Determines whether two artifacts have the same versionless identifier. This method is equivalent to calling
+     * {@link String#equals(Object)} on the return values from {@link #toVersionlessId(Artifact)} for the artifacts but
+     * does not incur the overhead of creating temporary strings.
+     * 
+     * @param artifact1 The first artifact, may be {@code null}.
+     * @param artifact2 The second artifact, may be {@code null}.
+     * @return {@code true} if both artifacts are not {@code null} and have equal versionless ids, {@code false}
+     *         otherwise.
+     */
+    public static boolean equalsVersionlessId( Artifact artifact1, Artifact artifact2 )
+    {
+        if ( artifact1 == null || artifact2 == null )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getArtifactId(), artifact2.getArtifactId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getGroupId(), artifact2.getGroupId() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getExtension(), artifact2.getExtension() ) )
+        {
+            return false;
+        }
+        if ( !eq( artifact1.getClassifier(), artifact2.getClassifier() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DefaultArtifactTypeRegistry.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DefaultArtifactTypeRegistry.java
new file mode 100644
index 0000000..9fb29af
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DefaultArtifactTypeRegistry.java
@@ -0,0 +1,51 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.ArtifactType;
+
+/**
+ * A simple artifact type registry.
+ */
+public final class DefaultArtifactTypeRegistry
+    extends SimpleArtifactTypeRegistry
+{
+
+    /**
+     * Creates a new artifact type registry with initally no registered artifact types. Use {@link #add(ArtifactType)}
+     * to populate the registry.
+     */
+    public DefaultArtifactTypeRegistry()
+    {
+    }
+
+    /**
+     * Adds the specified artifact type to the registry.
+     * 
+     * @param type The artifact type to add, must not be {@code null}.
+     * @return This registry for chaining, never {@code null}.
+     */
+    public DefaultArtifactTypeRegistry add( ArtifactType type )
+    {
+        super.add( type );
+        return this;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java
new file mode 100644
index 0000000..00fbcd4
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java
@@ -0,0 +1,166 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.AbstractArtifact;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * An artifact that delegates to another artifact instance. This class serves as a base for subclasses that want to
+ * carry additional data fields.
+ */
+public abstract class DelegatingArtifact
+    extends AbstractArtifact
+{
+
+    private final Artifact delegate;
+
+    /**
+     * Creates a new artifact instance that delegates to the specified artifact.
+     *
+     * @param delegate The artifact to delegate to, must not be {@code null}.
+     */
+    protected DelegatingArtifact( Artifact delegate )
+    {
+        this.delegate = requireNonNull( delegate, "delegate artifact cannot be null" );
+    }
+
+    /**
+     * Creates a new artifact instance that delegates to the specified artifact. Subclasses should use this hook to
+     * instantiate themselves, taking along any data from the current instance that was added.
+     *
+     * @param delegate The artifact to delegate to, must not be {@code null}.
+     * @return The new delegating artifact, never {@code null}.
+     */
+    protected abstract DelegatingArtifact newInstance( Artifact delegate );
+
+    public String getGroupId()
+    {
+        return delegate.getGroupId();
+    }
+
+    public String getArtifactId()
+    {
+        return delegate.getArtifactId();
+    }
+
+    public String getVersion()
+    {
+        return delegate.getVersion();
+    }
+
+    public Artifact setVersion( String version )
+    {
+        Artifact artifact = delegate.setVersion( version );
+        if ( artifact != delegate )
+        {
+            return newInstance( artifact );
+        }
+        return this;
+    }
+
+    public String getBaseVersion()
+    {
+        return delegate.getBaseVersion();
+    }
+
+    public boolean isSnapshot()
+    {
+        return delegate.isSnapshot();
+    }
+
+    public String getClassifier()
+    {
+        return delegate.getClassifier();
+    }
+
+    public String getExtension()
+    {
+        return delegate.getExtension();
+    }
+
+    public File getFile()
+    {
+        return delegate.getFile();
+    }
+
+    public Artifact setFile( File file )
+    {
+        Artifact artifact = delegate.setFile( file );
+        if ( artifact != delegate )
+        {
+            return newInstance( artifact );
+        }
+        return this;
+    }
+
+    public String getProperty( String key, String defaultValue )
+    {
+        return delegate.getProperty( key, defaultValue );
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return delegate.getProperties();
+    }
+
+    public Artifact setProperties( Map<String, String> properties )
+    {
+        Artifact artifact = delegate.setProperties( properties );
+        if ( artifact != delegate )
+        {
+            return newInstance( artifact );
+        }
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+
+        if ( obj instanceof DelegatingArtifact )
+        {
+            return delegate.equals( ( (DelegatingArtifact) obj ).delegate );
+        }
+
+        return delegate.equals( obj );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return delegate.hashCode();
+    }
+
+    @Override
+    public String toString()
+    {
+        return delegate.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/JavaScopes.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/JavaScopes.java
new file mode 100644
index 0000000..bf4894c
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/JavaScopes.java
@@ -0,0 +1,45 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+/**
+ * The dependency scopes used for Java dependencies.
+ * 
+ * @see org.eclipse.aether.graph.Dependency#getScope()
+ */
+public final class JavaScopes
+{
+
+    public static final String COMPILE = "compile";
+
+    public static final String PROVIDED = "provided";
+
+    public static final String SYSTEM = "system";
+
+    public static final String RUNTIME = "runtime";
+
+    public static final String TEST = "test";
+
+    private JavaScopes()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/OverlayArtifactTypeRegistry.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/OverlayArtifactTypeRegistry.java
new file mode 100644
index 0000000..6768b16
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/OverlayArtifactTypeRegistry.java
@@ -0,0 +1,70 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+
+/**
+ * An artifact type registry which first consults its own mappings and in case of an unknown type falls back to another
+ * type registry.
+ */
+public final class OverlayArtifactTypeRegistry
+    extends SimpleArtifactTypeRegistry
+{
+
+    private final ArtifactTypeRegistry delegate;
+
+    /**
+     * Creates a new artifact type registry with initially no registered artifact types and the specified fallback
+     * registry. Use {@link #add(ArtifactType)} to populate the registry.
+     * 
+     * @param delegate The artifact type registry to fall back to, may be {@code null}.
+     */
+    public OverlayArtifactTypeRegistry( ArtifactTypeRegistry delegate )
+    {
+        this.delegate = delegate;
+    }
+
+    /**
+     * Adds the specified artifact type to the registry.
+     * 
+     * @param type The artifact type to add, must not be {@code null}.
+     * @return This registry for chaining, never {@code null}.
+     */
+    public OverlayArtifactTypeRegistry add( ArtifactType type )
+    {
+        super.add( type );
+        return this;
+    }
+
+    public ArtifactType get( String typeId )
+    {
+        ArtifactType type = super.get( typeId );
+
+        if ( type == null && delegate != null )
+        {
+            type = delegate.get( typeId );
+        }
+
+        return type;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SimpleArtifactTypeRegistry.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SimpleArtifactTypeRegistry.java
new file mode 100644
index 0000000..b0bfc9f
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SimpleArtifactTypeRegistry.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+
+/**
+ * A simple map-based artifact type registry.
+ */
+class SimpleArtifactTypeRegistry
+    implements ArtifactTypeRegistry
+{
+
+    private final Map<String, ArtifactType> types;
+
+    /**
+     * Creates a new artifact type registry with initally no registered artifact types. Use {@link #add(ArtifactType)}
+     * to populate the registry.
+     */
+    public SimpleArtifactTypeRegistry()
+    {
+        types = new HashMap<String, ArtifactType>();
+    }
+
+    /**
+     * Adds the specified artifact type to the registry.
+     * 
+     * @param type The artifact type to add, must not be {@code null}.
+     * @return This registry for chaining, never {@code null}.
+     */
+    public SimpleArtifactTypeRegistry add( ArtifactType type )
+    {
+        types.put( type.getId(), type );
+        return this;
+    }
+
+    public ArtifactType get( String typeId )
+    {
+        ArtifactType type = types.get( typeId );
+
+        return type;
+    }
+
+    @Override
+    public String toString()
+    {
+        return types.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java
new file mode 100644
index 0000000..e0beb21
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java
@@ -0,0 +1,230 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.artifact.AbstractArtifact;
+import org.eclipse.aether.artifact.Artifact;
+
+/**
+ * An artifact whose identity is derived from another artifact. <em>Note:</em> Instances of this class are immutable and
+ * the exposed mutators return new objects rather than changing the current instance.
+ */
+public final class SubArtifact
+    extends AbstractArtifact
+{
+
+    private final Artifact mainArtifact;
+
+    private final String classifier;
+
+    private final String extension;
+
+    private final File file;
+
+    private final Map<String, String> properties;
+
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     * 
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     */
+    public SubArtifact( Artifact mainArtifact, String classifier, String extension )
+    {
+        this( mainArtifact, classifier, extension, (File) null );
+    }
+
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     * 
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     * @param file The file for this artifact, may be {@code null} if unresolved.
+     */
+    public SubArtifact( Artifact mainArtifact, String classifier, String extension, File file )
+    {
+        this( mainArtifact, classifier, extension, null, file );
+    }
+
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     * 
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     * @param properties The properties of the artifact, may be {@code null}.
+     */
+    public SubArtifact( Artifact mainArtifact, String classifier, String extension, Map<String, String> properties )
+    {
+        this( mainArtifact, classifier, extension, properties, null );
+    }
+
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     * 
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     * @param properties The properties of the artifact, may be {@code null}.
+     * @param file The file for this artifact, may be {@code null} if unresolved.
+     */
+    public SubArtifact( Artifact mainArtifact, String classifier, String extension, Map<String, String> properties,
+                        File file )
+    {
+        this.mainArtifact = requireNonNull( mainArtifact, "main artifact cannot be null" );
+        this.classifier = classifier;
+        this.extension = extension;
+        this.file = file;
+        this.properties = copyProperties( properties );
+    }
+
+    private SubArtifact( Artifact mainArtifact, String classifier, String extension, File file,
+                         Map<String, String> properties )
+    {
+        // NOTE: This constructor assumes immutability of the provided properties, for internal use only
+        this.mainArtifact = mainArtifact;
+        this.classifier = classifier;
+        this.extension = extension;
+        this.file = file;
+        this.properties = properties;
+    }
+
+    public String getGroupId()
+    {
+        return mainArtifact.getGroupId();
+    }
+
+    public String getArtifactId()
+    {
+        return mainArtifact.getArtifactId();
+    }
+
+    public String getVersion()
+    {
+        return mainArtifact.getVersion();
+    }
+
+    public String getBaseVersion()
+    {
+        return mainArtifact.getBaseVersion();
+    }
+
+    public boolean isSnapshot()
+    {
+        return mainArtifact.isSnapshot();
+    }
+
+    public String getClassifier()
+    {
+        return expand( classifier, mainArtifact.getClassifier() );
+    }
+
+    public String getExtension()
+    {
+        return expand( extension, mainArtifact.getExtension() );
+    }
+
+    public File getFile()
+    {
+        return file;
+    }
+
+    public Artifact setFile( File file )
+    {
+        if ( ( this.file == null ) ? file == null : this.file.equals( file ) )
+        {
+            return this;
+        }
+        return new SubArtifact( mainArtifact, classifier, extension, file, properties );
+    }
+
+    public Map<String, String> getProperties()
+    {
+        return properties;
+    }
+
+    public Artifact setProperties( Map<String, String> properties )
+    {
+        if ( this.properties.equals( properties ) || ( properties == null && this.properties.isEmpty() ) )
+        {
+            return this;
+        }
+        return new SubArtifact( mainArtifact, classifier, extension, properties, file );
+    }
+
+    private static String expand( String pattern, String replacement )
+    {
+        String result = "";
+        if ( pattern != null )
+        {
+            result = pattern.replace( "*", replacement );
+
+            if ( replacement.length() <= 0 )
+            {
+                if ( pattern.startsWith( "*" ) )
+                {
+                    int i = 0;
+                    for ( ; i < result.length(); i++ )
+                    {
+                        char c = result.charAt( i );
+                        if ( c != '-' && c != '.' )
+                        {
+                            break;
+                        }
+                    }
+                    result = result.substring( i );
+                }
+                if ( pattern.endsWith( "*" ) )
+                {
+                    int i = result.length() - 1;
+                    for ( ; i >= 0; i-- )
+                    {
+                        char c = result.charAt( i );
+                        if ( c != '-' && c != '.' )
+                        {
+                            break;
+                        }
+                    }
+                    result = result.substring( 0, i + 1 );
+                }
+            }
+        }
+        return result;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/package-info.java
new file mode 100644
index 0000000..153159f
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Utilities around artifacts and artifact type registries.
+ */
+package org.eclipse.aether.util.artifact;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/RunnableErrorForwarder.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/RunnableErrorForwarder.java
new file mode 100644
index 0000000..6bb2f9d
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/RunnableErrorForwarder.java
@@ -0,0 +1,151 @@
+package org.eclipse.aether.util.concurrency;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * A utility class to forward any uncaught {@link Error} or {@link RuntimeException} from a {@link Runnable} executed in
+ * a worker thread back to the parent thread. The simplified usage pattern looks like this:
+ * 
+ * <pre>
+ * RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
+ * for ( Runnable task : tasks )
+ * {
+ *     executor.execute( errorForwarder.wrap( task ) );
+ * }
+ * errorForwarder.await();
+ * </pre>
+ */
+public final class RunnableErrorForwarder
+{
+
+    private final Thread thread = Thread.currentThread();
+
+    private final AtomicInteger counter = new AtomicInteger();
+
+    private final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
+
+    /**
+     * Creates a new error forwarder for worker threads spawned by the current thread.
+     */
+    public RunnableErrorForwarder()
+    {
+    }
+
+    /**
+     * Wraps the specified runnable into an equivalent runnable that will allow forwarding of uncaught errors.
+     *
+     * @param runnable The runnable from which to forward errors, must not be {@code null}.
+     * @return The error-forwarding runnable to eventually execute, never {@code null}.
+     */
+    public Runnable wrap( final Runnable runnable )
+    {
+        requireNonNull( runnable, "runnable cannot be null" );
+
+        counter.incrementAndGet();
+
+        return new Runnable()
+        {
+            public void run()
+            {
+                try
+                {
+                    runnable.run();
+                }
+                catch ( RuntimeException e )
+                {
+                    error.compareAndSet( null, e );
+                    throw e;
+                }
+                catch ( Error e )
+                {
+                    error.compareAndSet( null, e );
+                    throw e;
+                }
+                finally
+                {
+                    counter.decrementAndGet();
+                    LockSupport.unpark( thread );
+                }
+            }
+        };
+    }
+
+    /**
+     * Causes the current thread to wait until all previously {@link #wrap(Runnable) wrapped} runnables have terminated
+     * and potentially re-throws an uncaught {@link RuntimeException} or {@link Error} from any of the runnables. In
+     * case multiple runnables encountered uncaught errors, one error is arbitrarily selected. <em>Note:</em> This
+     * method must be called from the same thread that created this error forwarder instance.
+     */
+    public void await()
+    {
+        awaitTerminationOfAllRunnables();
+
+        Throwable error = this.error.get();
+        if ( error != null )
+        {
+            if ( error instanceof RuntimeException )
+            {
+                throw (RuntimeException) error;
+            }
+            else if ( error instanceof ThreadDeath )
+            {
+                throw new IllegalStateException( error );
+            }
+            else if ( error instanceof Error )
+            {
+                throw (Error) error;
+            }
+            throw new IllegalStateException( error );
+        }
+    }
+
+    private void awaitTerminationOfAllRunnables()
+    {
+        if ( !thread.equals( Thread.currentThread() ) )
+        {
+            throw new IllegalStateException( "wrong caller thread, expected " + thread + " and not "
+                + Thread.currentThread() );
+        }
+
+        boolean interrupted = false;
+
+        while ( counter.get() > 0 )
+        {
+            LockSupport.park();
+
+            if ( Thread.interrupted() )
+            {
+                interrupted = true;
+            }
+        }
+
+        if ( interrupted )
+        {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/WorkerThreadFactory.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/WorkerThreadFactory.java
new file mode 100644
index 0000000..26d0fb6
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/WorkerThreadFactory.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.concurrency;
+
+/*
+ * 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.
+ */
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A factory to create worker threads with a given name prefix.
+ */
+public final class WorkerThreadFactory
+    implements ThreadFactory
+{
+
+    private final ThreadFactory factory;
+
+    private final String namePrefix;
+
+    private final AtomicInteger threadIndex;
+
+    private static final AtomicInteger POOL_INDEX = new AtomicInteger();
+
+    /**
+     * Creates a new thread factory whose threads will have names using the specified prefix.
+     * 
+     * @param namePrefix The prefix for the thread names, may be {@code null} or empty to derive the prefix from the
+     *            caller's simple class name.
+     */
+    public WorkerThreadFactory( String namePrefix )
+    {
+        this.factory = Executors.defaultThreadFactory();
+        this.namePrefix =
+            ( ( namePrefix != null && namePrefix.length() > 0 ) ? namePrefix : getCallerSimpleClassName() + '-' )
+                + POOL_INDEX.getAndIncrement() + '-';
+        threadIndex = new AtomicInteger();
+    }
+
+    private static String getCallerSimpleClassName()
+    {
+        StackTraceElement[] stack = new Exception().getStackTrace();
+        if ( stack == null || stack.length <= 2 )
+        {
+            return "Worker-";
+        }
+        String name = stack[2].getClassName();
+        name = name.substring( name.lastIndexOf( '.' ) + 1 );
+        return name;
+    }
+
+    public Thread newThread( Runnable r )
+    {
+        Thread thread = factory.newThread( r );
+        thread.setName( namePrefix + threadIndex.getAndIncrement() );
+        thread.setDaemon( true );
+        return thread;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/package-info.java
new file mode 100644
index 0000000..2bb7853
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Utilities for concurrent task processing.
+ */
+package org.eclipse.aether.util.concurrency;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AbstractPatternDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AbstractPatternDependencyFilter.java
new file mode 100644
index 0000000..d707d26
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AbstractPatternDependencyFilter.java
@@ -0,0 +1,232 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionRange;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ */
+class AbstractPatternDependencyFilter
+    implements DependencyFilter
+{
+
+    private final Set<String> patterns = new HashSet<String>();
+
+    private final VersionScheme versionScheme;
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public AbstractPatternDependencyFilter( final String... patterns )
+    {
+        this( null, patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be included.
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public AbstractPatternDependencyFilter( final VersionScheme versionScheme, final String... patterns )
+    {
+        this( versionScheme, patterns == null ? null : Arrays.asList( patterns ) );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public AbstractPatternDependencyFilter( final Collection<String> patterns )
+    {
+        this( null, patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns and {@link VersionScheme} .
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be included.
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public AbstractPatternDependencyFilter( final VersionScheme versionScheme, final Collection<String> patterns )
+    {
+        if ( patterns != null )
+        {
+            this.patterns.addAll( patterns );
+        }
+        this.versionScheme = versionScheme;
+    }
+
+    public boolean accept( final DependencyNode node, List<DependencyNode> parents )
+    {
+        final Dependency dependency = node.getDependency();
+        if ( dependency == null )
+        {
+            return true;
+        }
+        return accept( dependency.getArtifact() );
+    }
+
+    protected boolean accept( final Artifact artifact )
+    {
+        for ( final String pattern : patterns )
+        {
+            final boolean matched = accept( artifact, pattern );
+            if ( matched )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean accept( final Artifact artifact, final String pattern )
+    {
+        final String[] tokens =
+            new String[] { artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(),
+                artifact.getBaseVersion() };
+
+        final String[] patternTokens = pattern.split( ":" );
+
+        // fail immediately if pattern tokens outnumber tokens to match
+        boolean matched = ( patternTokens.length <= tokens.length );
+
+        for ( int i = 0; matched && i < patternTokens.length; i++ )
+        {
+            matched = matches( tokens[i], patternTokens[i] );
+        }
+
+        return matched;
+    }
+
+    private boolean matches( final String token, final String pattern )
+    {
+        boolean matches;
+
+        // support full wildcard and implied wildcard
+        if ( "*".equals( pattern ) || pattern.length() == 0 )
+        {
+            matches = true;
+        }
+        // support contains wildcard
+        else if ( pattern.startsWith( "*" ) && pattern.endsWith( "*" ) )
+        {
+            final String contains = pattern.substring( 1, pattern.length() - 1 );
+
+            matches = ( token.contains( contains ) );
+        }
+        // support leading wildcard
+        else if ( pattern.startsWith( "*" ) )
+        {
+            final String suffix = pattern.substring( 1, pattern.length() );
+
+            matches = token.endsWith( suffix );
+        }
+        // support trailing wildcard
+        else if ( pattern.endsWith( "*" ) )
+        {
+            final String prefix = pattern.substring( 0, pattern.length() - 1 );
+
+            matches = token.startsWith( prefix );
+        }
+        // support versions range
+        else if ( pattern.startsWith( "[" ) || pattern.startsWith( "(" ) )
+        {
+            matches = isVersionIncludedInRange( token, pattern );
+        }
+        // support exact match
+        else
+        {
+            matches = token.equals( pattern );
+        }
+
+        return matches;
+    }
+
+    private boolean isVersionIncludedInRange( final String version, final String range )
+    {
+        if ( versionScheme == null )
+        {
+            return false;
+        }
+        else
+        {
+            try
+            {
+                final Version parsedVersion = versionScheme.parseVersion( version );
+                final VersionRange parsedRange = versionScheme.parseVersionRange( range );
+
+                return parsedRange.containsVersion( parsedVersion );
+            }
+            catch ( final InvalidVersionSpecificationException e )
+            {
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public boolean equals( final Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        final AbstractPatternDependencyFilter that = (AbstractPatternDependencyFilter) obj;
+
+        return this.patterns.equals( that.patterns )
+            && ( this.versionScheme == null ? that.versionScheme == null
+                            : this.versionScheme.equals( that.versionScheme ) );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + patterns.hashCode();
+        hash = hash * 31 + ( ( versionScheme == null ) ? 0 : versionScheme.hashCode() );
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AndDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AndDependencyFilter.java
new file mode 100644
index 0000000..9997c94
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/AndDependencyFilter.java
@@ -0,0 +1,126 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency filter that combines zero or more other filters using a logical {@code AND}. The resulting filter
+ * accepts a given dependency node if and only if all constituent filters accept it.
+ */
+public final class AndDependencyFilter
+    implements DependencyFilter
+{
+
+    private final Set<DependencyFilter> filters = new LinkedHashSet<DependencyFilter>();
+
+    /**
+     * Creates a new filter from the specified filters. Prefer {@link #newInstance(DependencyFilter, DependencyFilter)}
+     * if any of the input filters might be {@code null}.
+     * 
+     * @param filters The filters to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencyFilter( DependencyFilter... filters )
+    {
+        if ( filters != null )
+        {
+            Collections.addAll( this.filters, filters );
+        }
+    }
+
+    /**
+     * Creates a new filter from the specified filters.
+     * 
+     * @param filters The filters to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencyFilter( Collection<DependencyFilter> filters )
+    {
+        if ( filters != null )
+        {
+            this.filters.addAll( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter from the specified filters.
+     * 
+     * @param filter1 The first filter to combine, may be {@code null}.
+     * @param filter2 The second filter to combine, may be {@code null}.
+     * @return The combined filter or {@code null} if both filter were {@code null}.
+     */
+    public static DependencyFilter newInstance( DependencyFilter filter1, DependencyFilter filter2 )
+    {
+        if ( filter1 == null )
+        {
+            return filter2;
+        }
+        else if ( filter2 == null )
+        {
+            return filter1;
+        }
+        return new AndDependencyFilter( filter1, filter2 );
+    }
+
+    public boolean accept( DependencyNode node, List<DependencyNode> parents )
+    {
+        for ( DependencyFilter filter : filters )
+        {
+            if ( !filter.accept( node, parents ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        AndDependencyFilter that = (AndDependencyFilter) obj;
+
+        return this.filters.equals( that.filters );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + filters.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/DependencyFilterUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/DependencyFilterUtils.java
new file mode 100644
index 0000000..887c4b1
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/DependencyFilterUtils.java
@@ -0,0 +1,199 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.util.artifact.JavaScopes;
+
+/**
+ * A utility class assisting in the creation of dependency node filters.
+ */
+public final class DependencyFilterUtils
+{
+
+    private DependencyFilterUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Creates a new filter that negates the specified filter.
+     * 
+     * @param filter The filter to negate, must not be {@code null}.
+     * @return The new filter, never {@code null}.
+     */
+    public static DependencyFilter notFilter( DependencyFilter filter )
+    {
+        return new NotDependencyFilter( filter );
+    }
+
+    /**
+     * Creates a new filter that combines the specified filters using a logical {@code AND}. If no filters are
+     * specified, the resulting filter accepts everything.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     * @return The new filter, never {@code null}.
+     */
+    public static DependencyFilter andFilter( DependencyFilter... filters )
+    {
+        if ( filters != null && filters.length == 1 )
+        {
+            return filters[0];
+        }
+        else
+        {
+            return new AndDependencyFilter( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter that combines the specified filters using a logical {@code AND}. If no filters are
+     * specified, the resulting filter accepts everything.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     * @return The new filter, never {@code null}.
+     */
+    public static DependencyFilter andFilter( Collection<DependencyFilter> filters )
+    {
+        if ( filters != null && filters.size() == 1 )
+        {
+            return filters.iterator().next();
+        }
+        else
+        {
+            return new AndDependencyFilter( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter that combines the specified filters using a logical {@code OR}. If no filters are specified,
+     * the resulting filter accepts nothing.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     * @return The new filter, never {@code null}.
+     */
+    public static DependencyFilter orFilter( DependencyFilter... filters )
+    {
+        if ( filters != null && filters.length == 1 )
+        {
+            return filters[0];
+        }
+        else
+        {
+            return new OrDependencyFilter( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter that combines the specified filters using a logical {@code OR}. If no filters are specified,
+     * the resulting filter accepts nothing.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     * @return The new filter, never {@code null}.
+     */
+    public static DependencyFilter orFilter( Collection<DependencyFilter> filters )
+    {
+        if ( filters != null && filters.size() == 1 )
+        {
+            return filters.iterator().next();
+        }
+        else
+        {
+            return new OrDependencyFilter( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter that selects dependencies whose scope matches one or more of the specified classpath types.
+     * A classpath type is a set of scopes separated by either {@code ','} or {@code '+'}.
+     * 
+     * @param classpathTypes The classpath types, may be {@code null} or empty to match no dependency.
+     * @return The new filter, never {@code null}.
+     * @see JavaScopes
+     */
+    public static DependencyFilter classpathFilter( String... classpathTypes )
+    {
+        return classpathFilter( ( classpathTypes != null ) ? Arrays.asList( classpathTypes ) : null );
+    }
+
+    /**
+     * Creates a new filter that selects dependencies whose scope matches one or more of the specified classpath types.
+     * A classpath type is a set of scopes separated by either {@code ','} or {@code '+'}.
+     * 
+     * @param classpathTypes The classpath types, may be {@code null} or empty to match no dependency.
+     * @return The new filter, never {@code null}.
+     * @see JavaScopes
+     */
+    public static DependencyFilter classpathFilter( Collection<String> classpathTypes )
+    {
+        Collection<String> types = new HashSet<String>();
+
+        if ( classpathTypes != null )
+        {
+            for ( String classpathType : classpathTypes )
+            {
+                String[] tokens = classpathType.split( "[+,]" );
+                for ( String token : tokens )
+                {
+                    token = token.trim();
+                    if ( token.length() > 0 )
+                    {
+                        types.add( token );
+                    }
+                }
+            }
+        }
+
+        Collection<String> included = new HashSet<String>();
+        for ( String type : types )
+        {
+            if ( JavaScopes.COMPILE.equals( type ) )
+            {
+                Collections.addAll( included, JavaScopes.COMPILE, JavaScopes.PROVIDED, JavaScopes.SYSTEM );
+            }
+            else if ( JavaScopes.RUNTIME.equals( type ) )
+            {
+                Collections.addAll( included, JavaScopes.COMPILE, JavaScopes.RUNTIME );
+            }
+            else if ( JavaScopes.TEST.equals( type ) )
+            {
+                Collections.addAll( included, JavaScopes.COMPILE, JavaScopes.PROVIDED, JavaScopes.SYSTEM,
+                                    JavaScopes.RUNTIME, JavaScopes.TEST );
+            }
+            else
+            {
+                included.add( type );
+            }
+        }
+
+        Collection<String> excluded = new HashSet<String>();
+        Collections.addAll( excluded, JavaScopes.COMPILE, JavaScopes.PROVIDED, JavaScopes.SYSTEM, JavaScopes.RUNTIME,
+                            JavaScopes.TEST );
+        excluded.removeAll( included );
+
+        return new ScopeDependencyFilter( null, excluded );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ExclusionsDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ExclusionsDependencyFilter.java
new file mode 100644
index 0000000..2de4ae8
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ExclusionsDependencyFilter.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A simple filter to exclude artifacts based on either artifact id or group id and artifact id.
+ */
+public final class ExclusionsDependencyFilter
+    implements DependencyFilter
+{
+
+    private final Set<String> excludes = new HashSet<String>();
+
+    /**
+     * Creates a new filter using the specified exclude patterns. A pattern can either be of the form
+     * {@code groupId:artifactId} (recommended) or just {@code artifactId} (deprecated).
+     * 
+     * @param excludes The exclude patterns, may be {@code null} or empty to exclude no artifacts.
+     */
+    public ExclusionsDependencyFilter( Collection<String> excludes )
+    {
+        if ( excludes != null )
+        {
+            this.excludes.addAll( excludes );
+        }
+    }
+
+    public boolean accept( DependencyNode node, List<DependencyNode> parents )
+    {
+        Dependency dependency = node.getDependency();
+
+        if ( dependency == null )
+        {
+            return true;
+        }
+
+        String id = dependency.getArtifact().getArtifactId();
+
+        if ( excludes.contains( id ) )
+        {
+            return false;
+        }
+
+        id = dependency.getArtifact().getGroupId() + ':' + id;
+
+        if ( excludes.contains( id ) )
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ExclusionsDependencyFilter that = (ExclusionsDependencyFilter) obj;
+
+        return this.excludes.equals( that.excludes );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + excludes.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/NotDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/NotDependencyFilter.java
new file mode 100644
index 0000000..dcb419a
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/NotDependencyFilter.java
@@ -0,0 +1,78 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency filter that negates another filter.
+ */
+public final class NotDependencyFilter
+    implements DependencyFilter
+{
+
+    private final DependencyFilter filter;
+
+    /**
+     * Creates a new filter negatint the specified filter.
+     *
+     * @param filter The filter to negate, must not be {@code null}.
+     */
+    public NotDependencyFilter( DependencyFilter filter )
+    {
+        this.filter = requireNonNull( filter, "dependency filter cannot be null" );
+    }
+
+    public boolean accept( DependencyNode node, List<DependencyNode> parents )
+    {
+        return !filter.accept( node, parents );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        NotDependencyFilter that = (NotDependencyFilter) obj;
+
+        return this.filter.equals( that.filter );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + filter.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/OrDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/OrDependencyFilter.java
new file mode 100644
index 0000000..665f6e7
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/OrDependencyFilter.java
@@ -0,0 +1,124 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency filter that combines zero or more other filters using a logical {@code OR}.
+ */
+public final class OrDependencyFilter
+    implements DependencyFilter
+{
+
+    private final Set<DependencyFilter> filters = new LinkedHashSet<DependencyFilter>();
+
+    /**
+     * Creates a new filter from the specified filters.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     */
+    public OrDependencyFilter( DependencyFilter... filters )
+    {
+        if ( filters != null )
+        {
+            Collections.addAll( this.filters, filters );
+        }
+    }
+
+    /**
+     * Creates a new filter from the specified filters.
+     * 
+     * @param filters The filters to combine, may be {@code null}.
+     */
+    public OrDependencyFilter( Collection<DependencyFilter> filters )
+    {
+        if ( filters != null )
+        {
+            this.filters.addAll( filters );
+        }
+    }
+
+    /**
+     * Creates a new filter from the specified filters.
+     * 
+     * @param filter1 The first filter to combine, may be {@code null}.
+     * @param filter2 The first filter to combine, may be {@code null}.
+     * @return The combined filter or {@code null} if both filter were {@code null}.
+     */
+    public static DependencyFilter newInstance( DependencyFilter filter1, DependencyFilter filter2 )
+    {
+        if ( filter1 == null )
+        {
+            return filter2;
+        }
+        else if ( filter2 == null )
+        {
+            return filter1;
+        }
+        return new OrDependencyFilter( filter1, filter2 );
+    }
+
+    public boolean accept( DependencyNode node, List<DependencyNode> parents )
+    {
+        for ( DependencyFilter filter : filters )
+        {
+            if ( filter.accept( node, parents ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        OrDependencyFilter that = (OrDependencyFilter) obj;
+
+        return this.filters.equals( that.filters );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + filters.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilter.java
new file mode 100644
index 0000000..e768614
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilter.java
@@ -0,0 +1,96 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * A simple filter to exclude artifacts from a list of patterns. The artifact pattern syntax is of the form:
+ * 
+ * <pre>
+ * [groupId]:[artifactId]:[extension]:[version]
+ * </pre>
+ * <p>
+ * Where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
+ * segment is treated as an implicit wildcard. Version can be a range in case a {@link VersionScheme} is specified.
+ * </p>
+ * <p>
+ * For example, <code>org.eclipse.*</code> would match all artifacts whose group id started with
+ * <code>org.eclipse.</code> , and <code>:::*-SNAPSHOT</code> would match all snapshot artifacts.
+ * </p>
+ */
+public final class PatternExclusionsDependencyFilter
+    extends AbstractPatternDependencyFilter
+{
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The exclude patterns, may be {@code null} or empty to exclude no artifacts.
+     */
+    public PatternExclusionsDependencyFilter( final String... patterns )
+    {
+        super( patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be excluded.
+     * @param patterns The exclude patterns, may be {@code null} or empty to exclude no artifacts.
+     */
+    public PatternExclusionsDependencyFilter( final VersionScheme versionScheme, final String... patterns )
+    {
+        super( versionScheme, patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public PatternExclusionsDependencyFilter( final Collection<String> patterns )
+    {
+        super( patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns and {@link VersionScheme} .
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be excluded.
+     * @param patterns The exclude patterns, may be {@code null} or empty to exclude no artifacts.
+     */
+    public PatternExclusionsDependencyFilter( final VersionScheme versionScheme, final Collection<String> patterns )
+    {
+        super( versionScheme, patterns );
+    }
+
+    @Override
+    protected boolean accept( Artifact artifact )
+    {
+        return !super.accept( artifact );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilter.java
new file mode 100644
index 0000000..e30600b
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilter.java
@@ -0,0 +1,89 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * A simple filter to include artifacts from a list of patterns. The artifact pattern syntax is of the form:
+ * 
+ * <pre>
+ * [groupId]:[artifactId]:[extension]:[version]
+ * </pre>
+ * <p>
+ * Where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
+ * segment is treated as an implicit wildcard. Version can be a range in case a {@link VersionScheme} is specified.
+ * </p>
+ * <p>
+ * For example, <code>org.eclipse.*</code> would match all artifacts whose group id started with
+ * <code>org.eclipse.</code> , and <code>:::*-SNAPSHOT</code> would match all snapshot artifacts.
+ * </p>
+ */
+public final class PatternInclusionsDependencyFilter
+    extends AbstractPatternDependencyFilter
+{
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public PatternInclusionsDependencyFilter( final String... patterns )
+    {
+        super( patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be included.
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public PatternInclusionsDependencyFilter( final VersionScheme versionScheme, final String... patterns )
+    {
+        super( versionScheme, patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns.
+     * 
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public PatternInclusionsDependencyFilter( final Collection<String> patterns )
+    {
+        super( patterns );
+    }
+
+    /**
+     * Creates a new filter using the specified patterns and {@link VersionScheme} .
+     * 
+     * @param versionScheme To be used for parsing versions/version ranges. If {@code null} and pattern specifies a
+     *            range no artifact will be included.
+     * @param patterns The include patterns, may be {@code null} or empty to include no artifacts.
+     */
+    public PatternInclusionsDependencyFilter( final VersionScheme versionScheme, final Collection<String> patterns )
+    {
+        super( versionScheme, patterns );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ScopeDependencyFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ScopeDependencyFilter.java
new file mode 100644
index 0000000..bc60c41
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/ScopeDependencyFilter.java
@@ -0,0 +1,118 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency filter based on dependency scopes. <em>Note:</em> This filter does not assume any relationships between
+ * the scopes. In particular, the filter is not aware of scopes that logically include other scopes.
+ * 
+ * @see Dependency#getScope()
+ */
+public final class ScopeDependencyFilter
+    implements DependencyFilter
+{
+
+    private final Set<String> included = new HashSet<String>();
+
+    private final Set<String> excluded = new HashSet<String>();
+
+    /**
+     * Creates a new filter using the specified includes and excludes.
+     * 
+     * @param included The set of scopes to include, may be {@code null} or empty to include any scope.
+     * @param excluded The set of scopes to exclude, may be {@code null} or empty to exclude no scope.
+     */
+    public ScopeDependencyFilter( Collection<String> included, Collection<String> excluded )
+    {
+        if ( included != null )
+        {
+            this.included.addAll( included );
+        }
+        if ( excluded != null )
+        {
+            this.excluded.addAll( excluded );
+        }
+    }
+
+    /**
+     * Creates a new filter using the specified excludes.
+     * 
+     * @param excluded The set of scopes to exclude, may be {@code null} or empty to exclude no scope.
+     */
+    public ScopeDependencyFilter( String... excluded )
+    {
+        if ( excluded != null )
+        {
+            this.excluded.addAll( Arrays.asList( excluded ) );
+        }
+    }
+
+    public boolean accept( DependencyNode node, List<DependencyNode> parents )
+    {
+        Dependency dependency = node.getDependency();
+
+        if ( dependency == null )
+        {
+            return true;
+        }
+
+        String scope = node.getDependency().getScope();
+        return ( included.isEmpty() || included.contains( scope ) )
+            && ( excluded.isEmpty() || !excluded.contains( scope ) );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ScopeDependencyFilter that = (ScopeDependencyFilter) obj;
+
+        return this.included.equals( that.included ) && this.excluded.equals( that.excluded );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + included.hashCode();
+        hash = hash * 31 + excluded.hashCode();
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/package-info.java
new file mode 100644
index 0000000..6547d2e
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/filter/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency filters for selecting nodes in a dependency graph.
+ */
+package org.eclipse.aether.util.filter;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManager.java
new file mode 100644
index 0000000..fefb9fb
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManager.java
@@ -0,0 +1,327 @@
+package org.eclipse.aether.util.graph.manager;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+import org.eclipse.aether.util.artifact.JavaScopes;
+
+/**
+ * A dependency manager that mimics the way Maven 2.x works.
+ */
+public final class ClassicDependencyManager
+    implements DependencyManager
+{
+
+    private final int depth;
+
+    private final Map<Object, String> managedVersions;
+
+    private final Map<Object, String> managedScopes;
+
+    private final Map<Object, Boolean> managedOptionals;
+
+    private final Map<Object, String> managedLocalPaths;
+
+    private final Map<Object, Collection<Exclusion>> managedExclusions;
+
+    private int hashCode;
+
+    /**
+     * Creates a new dependency manager without any management information.
+     */
+    public ClassicDependencyManager()
+    {
+        this( 0, Collections.<Object, String>emptyMap(), Collections.<Object, String>emptyMap(),
+              Collections.<Object, Boolean>emptyMap(), Collections.<Object, String>emptyMap(),
+              Collections.<Object, Collection<Exclusion>>emptyMap() );
+    }
+
+    private ClassicDependencyManager( int depth, Map<Object, String> managedVersions,
+                                      Map<Object, String> managedScopes, Map<Object, Boolean> managedOptionals,
+                                      Map<Object, String> managedLocalPaths,
+                                      Map<Object, Collection<Exclusion>> managedExclusions )
+    {
+        this.depth = depth;
+        this.managedVersions = managedVersions;
+        this.managedScopes = managedScopes;
+        this.managedOptionals = managedOptionals;
+        this.managedLocalPaths = managedLocalPaths;
+        this.managedExclusions = managedExclusions;
+    }
+
+    public DependencyManager deriveChildManager( DependencyCollectionContext context )
+    {
+        if ( depth >= 2 )
+        {
+            return this;
+        }
+        else if ( depth == 1 )
+        {
+            return new ClassicDependencyManager( depth + 1, managedVersions, managedScopes, managedOptionals,
+                                                 managedLocalPaths, managedExclusions );
+        }
+
+        Map<Object, String> managedVersions = this.managedVersions;
+        Map<Object, String> managedScopes = this.managedScopes;
+        Map<Object, Boolean> managedOptionals = this.managedOptionals;
+        Map<Object, String> managedLocalPaths = this.managedLocalPaths;
+        Map<Object, Collection<Exclusion>> managedExclusions = this.managedExclusions;
+
+        for ( Dependency managedDependency : context.getManagedDependencies() )
+        {
+            Artifact artifact = managedDependency.getArtifact();
+            Object key = getKey( artifact );
+
+            String version = artifact.getVersion();
+            if ( version.length() > 0 && !managedVersions.containsKey( key ) )
+            {
+                if ( managedVersions == this.managedVersions )
+                {
+                    managedVersions = new HashMap<Object, String>( this.managedVersions );
+                }
+                managedVersions.put( key, version );
+            }
+
+            String scope = managedDependency.getScope();
+            if ( scope.length() > 0 && !managedScopes.containsKey( key ) )
+            {
+                if ( managedScopes == this.managedScopes )
+                {
+                    managedScopes = new HashMap<Object, String>( this.managedScopes );
+                }
+                managedScopes.put( key, scope );
+            }
+
+            Boolean optional = managedDependency.getOptional();
+            if ( optional != null && !managedOptionals.containsKey( key ) )
+            {
+                if ( managedOptionals == this.managedOptionals )
+                {
+                    managedOptionals = new HashMap<Object, Boolean>( this.managedOptionals );
+                }
+                managedOptionals.put( key, optional );
+            }
+
+            String localPath = managedDependency.getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null );
+            if ( localPath != null && !managedLocalPaths.containsKey( key ) )
+            {
+                if ( managedLocalPaths == this.managedLocalPaths )
+                {
+                    managedLocalPaths = new HashMap<Object, String>( this.managedLocalPaths );
+                }
+                managedLocalPaths.put( key, localPath );
+            }
+
+            Collection<Exclusion> exclusions = managedDependency.getExclusions();
+            if ( !exclusions.isEmpty() )
+            {
+                if ( managedExclusions == this.managedExclusions )
+                {
+                    managedExclusions = new HashMap<Object, Collection<Exclusion>>( this.managedExclusions );
+                }
+                Collection<Exclusion> managed = managedExclusions.get( key );
+                if ( managed == null )
+                {
+                    managed = new LinkedHashSet<Exclusion>();
+                    managedExclusions.put( key, managed );
+                }
+                managed.addAll( exclusions );
+            }
+        }
+
+        return new ClassicDependencyManager( depth + 1, managedVersions, managedScopes, managedOptionals,
+                                             managedLocalPaths, managedExclusions );
+    }
+
+    public DependencyManagement manageDependency( Dependency dependency )
+    {
+        DependencyManagement management = null;
+
+        Object key = getKey( dependency.getArtifact() );
+
+        if ( depth >= 2 )
+        {
+            String version = managedVersions.get( key );
+            if ( version != null )
+            {
+                if ( management == null )
+                {
+                    management = new DependencyManagement();
+                }
+                management.setVersion( version );
+            }
+
+            String scope = managedScopes.get( key );
+            if ( scope != null )
+            {
+                if ( management == null )
+                {
+                    management = new DependencyManagement();
+                }
+                management.setScope( scope );
+
+                if ( !JavaScopes.SYSTEM.equals( scope )
+                    && dependency.getArtifact().getProperty( ArtifactProperties.LOCAL_PATH, null ) != null )
+                {
+                    Map<String, String> properties =
+                        new HashMap<String, String>( dependency.getArtifact().getProperties() );
+                    properties.remove( ArtifactProperties.LOCAL_PATH );
+                    management.setProperties( properties );
+                }
+            }
+
+            if ( ( scope != null && JavaScopes.SYSTEM.equals( scope ) )
+                || ( scope == null && JavaScopes.SYSTEM.equals( dependency.getScope() ) ) )
+            {
+                String localPath = managedLocalPaths.get( key );
+                if ( localPath != null )
+                {
+                    if ( management == null )
+                    {
+                        management = new DependencyManagement();
+                    }
+                    Map<String, String> properties =
+                        new HashMap<String, String>( dependency.getArtifact().getProperties() );
+                    properties.put( ArtifactProperties.LOCAL_PATH, localPath );
+                    management.setProperties( properties );
+                }
+            }
+
+            Boolean optional = managedOptionals.get( key );
+            if ( optional != null )
+            {
+                if ( management == null )
+                {
+                    management = new DependencyManagement();
+                }
+                management.setOptional( optional );
+            }
+        }
+
+        Collection<Exclusion> exclusions = managedExclusions.get( key );
+        if ( exclusions != null )
+        {
+            if ( management == null )
+            {
+                management = new DependencyManagement();
+            }
+            Collection<Exclusion> result = new LinkedHashSet<Exclusion>( dependency.getExclusions() );
+            result.addAll( exclusions );
+            management.setExclusions( result );
+        }
+
+        return management;
+    }
+
+    private Object getKey( Artifact a )
+    {
+        return new Key( a );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ClassicDependencyManager that = (ClassicDependencyManager) obj;
+        return depth == that.depth && managedVersions.equals( that.managedVersions )
+            && managedScopes.equals( that.managedScopes ) && managedOptionals.equals( that.managedOptionals )
+            && managedExclusions.equals( that.managedExclusions );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = 17;
+            hash = hash * 31 + depth;
+            hash = hash * 31 + managedVersions.hashCode();
+            hash = hash * 31 + managedScopes.hashCode();
+            hash = hash * 31 + managedOptionals.hashCode();
+            hash = hash * 31 + managedExclusions.hashCode();
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+    static class Key
+    {
+
+        private final Artifact artifact;
+
+        private final int hashCode;
+
+        public Key( Artifact artifact )
+        {
+            this.artifact = artifact;
+
+            int hash = 17;
+            hash = hash * 31 + artifact.getGroupId().hashCode();
+            hash = hash * 31 + artifact.getArtifactId().hashCode();
+            hashCode = hash;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof Key ) )
+            {
+                return false;
+            }
+            Key that = (Key) obj;
+            return artifact.getArtifactId().equals( that.artifact.getArtifactId() )
+                && artifact.getGroupId().equals( that.artifact.getGroupId() )
+                && artifact.getExtension().equals( that.artifact.getExtension() )
+                && artifact.getClassifier().equals( that.artifact.getClassifier() );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return hashCode;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java
new file mode 100644
index 0000000..f549367
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java
@@ -0,0 +1,174 @@
+package org.eclipse.aether.util.graph.manager;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.Exclusion;
+
+/**
+ * A utility class assisting in analyzing the effects of dependency management.
+ */
+public final class DependencyManagerUtils
+{
+
+    /**
+     * The key in the repository session's {@link RepositorySystemSession#getConfigProperties() configuration
+     * properties} used to store a {@link Boolean} flag controlling the verbose mode for dependency management. If
+     * enabled, the original attributes of a dependency before its update due to dependency managemnent will be recorded
+     * in the node's {@link DependencyNode#getData() custom data} when building a dependency graph.
+     */
+    public static final String CONFIG_PROP_VERBOSE = "aether.dependencyManager.verbose";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the original version is
+     * stored.
+     */
+    public static final String NODE_DATA_PREMANAGED_VERSION = "premanaged.version";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the original scope is
+     * stored.
+     */
+    public static final String NODE_DATA_PREMANAGED_SCOPE = "premanaged.scope";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the original optional
+     * flag is stored.
+     */
+    public static final String NODE_DATA_PREMANAGED_OPTIONAL = "premanaged.optional";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the original exclusions
+     * are stored.
+     *
+     * @since 1.1.0
+     */
+    public static final String NODE_DATA_PREMANAGED_EXCLUSIONS = "premanaged.exclusions";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the original properties
+     * are stored.
+     *
+     * @since 1.1.0
+     */
+    public static final String NODE_DATA_PREMANAGED_PROPERTIES = "premanaged.properties";
+
+    /**
+     * Gets the version or version range of the specified dependency node before dependency management was applied (if
+     * any).
+     *
+     * @param node The dependency node to retrieve the premanaged data for, must not be {@code null}.
+     *
+     * @return The node's dependency version before dependency management or {@code null} if the version was not managed
+     *         or if {@link #CONFIG_PROP_VERBOSE} was not enabled.
+     */
+    public static String getPremanagedVersion( DependencyNode node )
+    {
+        if ( ( node.getManagedBits() & DependencyNode.MANAGED_VERSION ) == 0 )
+        {
+            return null;
+        }
+        return cast( node.getData().get( NODE_DATA_PREMANAGED_VERSION ), String.class );
+    }
+
+    /**
+     * Gets the scope of the specified dependency node before dependency management was applied (if any).
+     *
+     * @param node The dependency node to retrieve the premanaged data for, must not be {@code null}.
+     *
+     * @return The node's dependency scope before dependency management or {@code null} if the scope was not managed or
+     *         if {@link #CONFIG_PROP_VERBOSE} was not enabled.
+     */
+    public static String getPremanagedScope( DependencyNode node )
+    {
+        if ( ( node.getManagedBits() & DependencyNode.MANAGED_SCOPE ) == 0 )
+        {
+            return null;
+        }
+        return cast( node.getData().get( NODE_DATA_PREMANAGED_SCOPE ), String.class );
+    }
+
+    /**
+     * Gets the optional flag of the specified dependency node before dependency management was applied (if any).
+     *
+     * @param node The dependency node to retrieve the premanaged data for, must not be {@code null}.
+     *
+     * @return The node's optional flag before dependency management or {@code null} if the flag was not managed or if
+     *         {@link #CONFIG_PROP_VERBOSE} was not enabled.
+     */
+    public static Boolean getPremanagedOptional( DependencyNode node )
+    {
+        if ( ( node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL ) == 0 )
+        {
+            return null;
+        }
+        return cast( node.getData().get( NODE_DATA_PREMANAGED_OPTIONAL ), Boolean.class );
+    }
+
+    /**
+     * Gets the {@code Exclusion}s of the specified dependency node before dependency management was applied (if any).
+     *
+     * @param node The dependency node to retrieve the premanaged data for, must not be {@code null}.
+     *
+     * @return The nodes' {@code Exclusion}s before dependency management or {@code null} if exclusions were not managed
+     *         or if {@link #CONFIG_PROP_VERBOSE} was not enabled.
+     *
+     * @since 1.1.0
+     */
+    @SuppressWarnings( "unchecked" )
+    public static Collection<Exclusion> getPremanagedExclusions( DependencyNode node )
+    {
+        if ( ( node.getManagedBits() & DependencyNode.MANAGED_EXCLUSIONS ) == 0 )
+        {
+            return null;
+        }
+        return cast( node.getData().get( NODE_DATA_PREMANAGED_EXCLUSIONS ), Collection.class );
+    }
+
+    /**
+     * Gets the properties of the specified dependency node before dependency management was applied (if any).
+     *
+     * @param node The dependency node to retrieve the premanaged data for, must not be {@code null}.
+     *
+     * @return The nodes' properties before dependency management or {@code null} if properties were not managed or if
+     *         {@link #CONFIG_PROP_VERBOSE} was not enabled.
+     *
+     * @since 1.1.0
+     */
+    @SuppressWarnings( "unchecked" )
+    public static Map<String, String> getPremanagedProperties( DependencyNode node )
+    {
+        if ( ( node.getManagedBits() & DependencyNode.MANAGED_PROPERTIES ) == 0 )
+        {
+            return null;
+        }
+        return cast( node.getData().get( NODE_DATA_PREMANAGED_PROPERTIES ), Map.class );
+    }
+
+    private static <T> T cast( Object obj, Class<T> type )
+    {
+        return type.isInstance( obj ) ? type.cast( obj ) : null;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/NoopDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/NoopDependencyManager.java
new file mode 100644
index 0000000..ae8ee40
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/NoopDependencyManager.java
@@ -0,0 +1,77 @@
+package org.eclipse.aether.util.graph.manager;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency manager that does not do any dependency management.
+ */
+public final class NoopDependencyManager
+    implements DependencyManager
+{
+
+    /**
+     * A ready-made instance of this dependency manager which can safely be reused throughout an entire application
+     * regardless of multi-threading.
+     */
+    public static final DependencyManager INSTANCE = new NoopDependencyManager();
+
+    /**
+     * Creates a new instance of this dependency manager. Usually, {@link #INSTANCE} should be used instead.
+     */
+    public NoopDependencyManager()
+    {
+    }
+
+    public DependencyManager deriveChildManager( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    public DependencyManagement manageDependency( Dependency dependency )
+    {
+        return null;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/package-info.java
new file mode 100644
index 0000000..7c7ae12
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency managers for building a dependency graph.
+ */
+package org.eclipse.aether.util.graph.manager;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/AndDependencySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/AndDependencySelector.java
new file mode 100644
index 0000000..f2a7e38
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/AndDependencySelector.java
@@ -0,0 +1,206 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency selector that combines zero or more other selectors using a logical {@code AND}. The resulting selector
+ * selects a given dependency if and only if all constituent selectors do so.
+ */
+public final class AndDependencySelector
+    implements DependencySelector
+{
+
+    private final Set<? extends DependencySelector> selectors;
+
+    private int hashCode;
+
+    /**
+     * Creates a new selector from the specified selectors. Prefer
+     * {@link #newInstance(DependencySelector, DependencySelector)} if any of the input selectors might be {@code null}.
+     * 
+     * @param selectors The selectors to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencySelector( DependencySelector... selectors )
+    {
+        if ( selectors != null && selectors.length > 0 )
+        {
+            this.selectors = new LinkedHashSet<DependencySelector>( Arrays.asList( selectors ) );
+        }
+        else
+        {
+            this.selectors = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Creates a new selector from the specified selectors.
+     * 
+     * @param selectors The selectors to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencySelector( Collection<? extends DependencySelector> selectors )
+    {
+        if ( selectors != null && !selectors.isEmpty() )
+        {
+            this.selectors = new LinkedHashSet<DependencySelector>( selectors );
+        }
+        else
+        {
+            this.selectors = Collections.emptySet();
+        }
+    }
+
+    private AndDependencySelector( Set<DependencySelector> selectors )
+    {
+        if ( selectors != null && !selectors.isEmpty() )
+        {
+            this.selectors = selectors;
+        }
+        else
+        {
+            this.selectors = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Creates a new selector from the specified selectors.
+     * 
+     * @param selector1 The first selector to combine, may be {@code null}.
+     * @param selector2 The second selector to combine, may be {@code null}.
+     * @return The combined selector or {@code null} if both selectors were {@code null}.
+     */
+    public static DependencySelector newInstance( DependencySelector selector1, DependencySelector selector2 )
+    {
+        if ( selector1 == null )
+        {
+            return selector2;
+        }
+        else if ( selector2 == null || selector2.equals( selector1 ) )
+        {
+            return selector1;
+        }
+        return new AndDependencySelector( selector1, selector2 );
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        for ( DependencySelector selector : selectors )
+        {
+            if ( !selector.selectDependency( dependency ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+    {
+        int seen = 0;
+        Set<DependencySelector> childSelectors = null;
+
+        for ( DependencySelector selector : selectors )
+        {
+            DependencySelector childSelector = selector.deriveChildSelector( context );
+            if ( childSelectors != null )
+            {
+                if ( childSelector != null )
+                {
+                    childSelectors.add( childSelector );
+                }
+            }
+            else if ( selector != childSelector )
+            {
+                childSelectors = new LinkedHashSet<DependencySelector>();
+                if ( seen > 0 )
+                {
+                    for ( DependencySelector s : selectors )
+                    {
+                        if ( childSelectors.size() >= seen )
+                        {
+                            break;
+                        }
+                        childSelectors.add( s );
+                    }
+                }
+                if ( childSelector != null )
+                {
+                    childSelectors.add( childSelector );
+                }
+            }
+            else
+            {
+                seen++;
+            }
+        }
+
+        if ( childSelectors == null )
+        {
+            return this;
+        }
+        if ( childSelectors.size() <= 1 )
+        {
+            if ( childSelectors.isEmpty() )
+            {
+                return null;
+            }
+            return childSelectors.iterator().next();
+        }
+        return new AndDependencySelector( childSelectors );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        AndDependencySelector that = (AndDependencySelector) obj;
+        return selectors.equals( that.selectors );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = 17;
+            hash = hash * 31 + selectors.hashCode();
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ExclusionDependencySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ExclusionDependencySelector.java
new file mode 100644
index 0000000..221cf4f
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ExclusionDependencySelector.java
@@ -0,0 +1,227 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.Exclusion;
+
+/**
+ * A dependency selector that applies exclusions based on artifact coordinates.
+ * 
+ * @see Dependency#getExclusions()
+ */
+public final class ExclusionDependencySelector
+    implements DependencySelector
+{
+
+    // sorted and dupe-free array, faster to iterate than LinkedHashSet
+    private final Exclusion[] exclusions;
+
+    private int hashCode;
+
+    /**
+     * Creates a new selector without any exclusions.
+     */
+    public ExclusionDependencySelector()
+    {
+        this.exclusions = new Exclusion[0];
+    }
+
+    /**
+     * Creates a new selector with the specified exclusions.
+     * 
+     * @param exclusions The exclusions, may be {@code null}.
+     */
+    public ExclusionDependencySelector( Collection<Exclusion> exclusions )
+    {
+        if ( exclusions != null && !exclusions.isEmpty() )
+        {
+            TreeSet<Exclusion> sorted = new TreeSet<Exclusion>( ExclusionComparator.INSTANCE );
+            sorted.addAll( exclusions );
+            this.exclusions = sorted.toArray( new Exclusion[sorted.size()] );
+        }
+        else
+        {
+            this.exclusions = new Exclusion[0];
+        }
+    }
+
+    private ExclusionDependencySelector( Exclusion[] exclusions )
+    {
+        this.exclusions = exclusions;
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        Artifact artifact = dependency.getArtifact();
+        for ( Exclusion exclusion : exclusions )
+        {
+            if ( matches( exclusion, artifact ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean matches( Exclusion exclusion, Artifact artifact )
+    {
+        if ( !matches( exclusion.getArtifactId(), artifact.getArtifactId() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getGroupId(), artifact.getGroupId() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getExtension(), artifact.getExtension() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getClassifier(), artifact.getClassifier() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean matches( String pattern, String value )
+    {
+        return "*".equals( pattern ) || pattern.equals( value );
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+    {
+        Dependency dependency = context.getDependency();
+        Collection<Exclusion> exclusions = ( dependency != null ) ? dependency.getExclusions() : null;
+        if ( exclusions == null || exclusions.isEmpty() )
+        {
+            return this;
+        }
+
+        Exclusion[] merged = this.exclusions;
+        int count = merged.length;
+        for ( Exclusion exclusion : exclusions )
+        {
+            int index = Arrays.binarySearch( merged, exclusion, ExclusionComparator.INSTANCE );
+            if ( index < 0 )
+            {
+                index = -( index + 1 );
+                if ( count >= merged.length )
+                {
+                    Exclusion[] tmp = new Exclusion[merged.length + exclusions.size()];
+                    System.arraycopy( merged, 0, tmp, 0, index );
+                    tmp[index] = exclusion;
+                    System.arraycopy( merged, index, tmp, index + 1, count - index );
+                    merged = tmp;
+                }
+                else
+                {
+                    System.arraycopy( merged, index, merged, index + 1, count - index );
+                    merged[index] = exclusion;
+                }
+                count++;
+            }
+        }
+        if ( merged == this.exclusions )
+        {
+            return this;
+        }
+        if ( merged.length != count )
+        {
+            Exclusion[] tmp = new Exclusion[count];
+            System.arraycopy( merged, 0, tmp, 0, count );
+            merged = tmp;
+        }
+
+        return new ExclusionDependencySelector( merged );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ExclusionDependencySelector that = (ExclusionDependencySelector) obj;
+        return Arrays.equals( exclusions, that.exclusions );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = getClass().hashCode();
+            hash = hash * 31 + Arrays.hashCode( exclusions );
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+    private static class ExclusionComparator
+        implements Comparator<Exclusion>
+    {
+
+        static final ExclusionComparator INSTANCE = new ExclusionComparator();
+
+        public int compare( Exclusion e1, Exclusion e2 )
+        {
+            if ( e1 == null )
+            {
+                return ( e2 == null ) ? 0 : 1;
+            }
+            else if ( e2 == null )
+            {
+                return -1;
+            }
+            int rel = e1.getArtifactId().compareTo( e2.getArtifactId() );
+            if ( rel == 0 )
+            {
+                rel = e1.getGroupId().compareTo( e2.getGroupId() );
+                if ( rel == 0 )
+                {
+                    rel = e1.getExtension().compareTo( e2.getExtension() );
+                    if ( rel == 0 )
+                    {
+                        rel = e1.getClassifier().compareTo( e2.getClassifier() );
+                    }
+                }
+            }
+            return rel;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/OptionalDependencySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/OptionalDependencySelector.java
new file mode 100644
index 0000000..69bbda4
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/OptionalDependencySelector.java
@@ -0,0 +1,89 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency selector that excludes optional dependencies which occur beyond level one of the dependency graph.
+ * 
+ * @see Dependency#isOptional()
+ */
+public final class OptionalDependencySelector
+    implements DependencySelector
+{
+
+    private final int depth;
+
+    /**
+     * Creates a new selector to exclude optional transitive dependencies.
+     */
+    public OptionalDependencySelector()
+    {
+        depth = 0;
+    }
+
+    private OptionalDependencySelector( int depth )
+    {
+        this.depth = depth;
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        return depth < 2 || !dependency.isOptional();
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+    {
+        if ( depth >= 2 )
+        {
+            return this;
+        }
+
+        return new OptionalDependencySelector( depth + 1 );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        OptionalDependencySelector that = (OptionalDependencySelector) obj;
+        return depth == that.depth;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + depth;
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ScopeDependencySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ScopeDependencySelector.java
new file mode 100644
index 0000000..552fc09
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/ScopeDependencySelector.java
@@ -0,0 +1,151 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.TreeSet;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency selector that filters transitive dependencies based on their scope. Direct dependencies are always
+ * included regardless of their scope. <em>Note:</em> This filter does not assume any relationships between the scopes.
+ * In particular, the filter is not aware of scopes that logically include other scopes.
+ * 
+ * @see Dependency#getScope()
+ */
+public final class ScopeDependencySelector
+    implements DependencySelector
+{
+
+    private final boolean transitive;
+
+    private final Collection<String> included;
+
+    private final Collection<String> excluded;
+
+    /**
+     * Creates a new selector using the specified includes and excludes.
+     * 
+     * @param included The set of scopes to include, may be {@code null} or empty to include any scope.
+     * @param excluded The set of scopes to exclude, may be {@code null} or empty to exclude no scope.
+     */
+    public ScopeDependencySelector( Collection<String> included, Collection<String> excluded )
+    {
+        transitive = false;
+        this.included = clone( included );
+        this.excluded = clone( excluded );
+    }
+
+    private static Collection<String> clone( Collection<String> scopes )
+    {
+        Collection<String> copy;
+        if ( scopes == null || scopes.isEmpty() )
+        {
+            // checking for null is faster than isEmpty()
+            copy = null;
+        }
+        else
+        {
+            copy = new HashSet<String>( scopes );
+            if ( copy.size() <= 2 )
+            {
+                // contains() is faster for smallish array (sorted for equals()!)
+                copy = new ArrayList<String>( new TreeSet<String>( copy ) );
+            }
+        }
+        return copy;
+    }
+
+    /**
+     * Creates a new selector using the specified excludes.
+     * 
+     * @param excluded The set of scopes to exclude, may be {@code null} or empty to exclude no scope.
+     */
+    public ScopeDependencySelector( String... excluded )
+    {
+        this( null, ( excluded != null ) ? Arrays.asList( excluded ) : null );
+    }
+
+    private ScopeDependencySelector( boolean transitive, Collection<String> included, Collection<String> excluded )
+    {
+        this.transitive = transitive;
+        this.included = included;
+        this.excluded = excluded;
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        if ( !transitive )
+        {
+            return true;
+        }
+
+        String scope = dependency.getScope();
+        return ( included == null || included.contains( scope ) ) && ( excluded == null || !excluded.contains( scope ) );
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+    {
+        if ( this.transitive || context.getDependency() == null )
+        {
+            return this;
+        }
+
+        return new ScopeDependencySelector( true, included, excluded );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ScopeDependencySelector that = (ScopeDependencySelector) obj;
+        return transitive == that.transitive && eq( included, that.included ) && eq( excluded, that.excluded );
+    }
+
+    private static <T> boolean eq( T o1, T o2 )
+    {
+        return ( o1 != null ) ? o1.equals( o2 ) : o2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + ( transitive ? 1 : 0 );
+        hash = hash * 31 + ( included != null ? included.hashCode() : 0 );
+        hash = hash * 31 + ( excluded != null ? excluded.hashCode() : 0 );
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/StaticDependencySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/StaticDependencySelector.java
new file mode 100644
index 0000000..41ce0e0
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/StaticDependencySelector.java
@@ -0,0 +1,79 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency selector that always includes or excludes dependencies.
+ */
+public final class StaticDependencySelector
+    implements DependencySelector
+{
+
+    private final boolean select;
+
+    /**
+     * Creates a new selector with the specified selection behavior.
+     * 
+     * @param select {@code true} to select all dependencies, {@code false} to exclude all dependencies.
+     */
+    public StaticDependencySelector( boolean select )
+    {
+        this.select = select;
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        return select;
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        StaticDependencySelector that = (StaticDependencySelector) obj;
+        return select == that.select;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + ( select ? 1 : 0 );
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/package-info.java
new file mode 100644
index 0000000..3c3cf3c
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/selector/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency selectors for building a dependency graph.
+ */
+package org.eclipse.aether.util.graph.selector;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ChainedDependencyGraphTransformer.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ChainedDependencyGraphTransformer.java
new file mode 100644
index 0000000..d7f1771
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ChainedDependencyGraphTransformer.java
@@ -0,0 +1,85 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency graph transformer that chains other transformers.
+ */
+public final class ChainedDependencyGraphTransformer
+    implements DependencyGraphTransformer
+{
+
+    private final DependencyGraphTransformer[] transformers;
+
+    /**
+     * Creates a new transformer that chains the specified transformers.
+     * 
+     * @param transformers The transformers to chain, may be {@code null} or empty.
+     */
+    public ChainedDependencyGraphTransformer( DependencyGraphTransformer... transformers )
+    {
+        if ( transformers == null )
+        {
+            this.transformers = new DependencyGraphTransformer[0];
+        }
+        else
+        {
+            this.transformers = transformers;
+        }
+    }
+
+    /**
+     * Creates a new transformer that chains the specified transformers or simply returns one of them if the other one
+     * is {@code null}.
+     * 
+     * @param transformer1 The first transformer of the chain, may be {@code null}.
+     * @param transformer2 The second transformer of the chain, may be {@code null}.
+     * @return The chained transformer or {@code null} if both input transformers are {@code null}.
+     */
+    public static DependencyGraphTransformer newInstance( DependencyGraphTransformer transformer1,
+                                                          DependencyGraphTransformer transformer2 )
+    {
+        if ( transformer1 == null )
+        {
+            return transformer2;
+        }
+        else if ( transformer2 == null )
+        {
+            return transformer1;
+        }
+        return new ChainedDependencyGraphTransformer( transformer1, transformer2 );
+    }
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        for ( DependencyGraphTransformer transformer : transformers )
+        {
+            node = transformer.transformGraph( node, context );
+        }
+        return node;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorter.java
new file mode 100644
index 0000000..5cc6432
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorter.java
@@ -0,0 +1,370 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency graph transformer that creates a topological sorting of the conflict ids which have been assigned to the
+ * dependency nodes. Conflict ids are sorted according to the dependency relation induced by the dependency graph. This
+ * transformer will query the key {@link TransformationContextKeys#CONFLICT_IDS} in the transformation context for an
+ * existing mapping of nodes to their conflicts ids. In absence of this map, the transformer will automatically invoke
+ * the {@link ConflictMarker} to calculate the conflict ids. When this transformer has executed, the transformation
+ * context holds a {@code List<Object>} that denotes the topologically sorted conflict ids. The list will be stored
+ * using the key {@link TransformationContextKeys#SORTED_CONFLICT_IDS}. In addition, the transformer will store a
+ * {@code Collection<Collection<Object>>} using the key {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} that
+ * describes cycles among conflict ids.
+ */
+public final class ConflictIdSorter
+    implements DependencyGraphTransformer
+{
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        Map<?, ?> conflictIds = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        if ( conflictIds == null )
+        {
+            ConflictMarker marker = new ConflictMarker();
+            marker.transformGraph( node, context );
+
+            conflictIds = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        }
+
+        @SuppressWarnings( "unchecked" )
+        Map<String, Object> stats = (Map<String, Object>) context.get( TransformationContextKeys.STATS );
+        long time1 = System.nanoTime();
+
+        Map<Object, ConflictId> ids = new LinkedHashMap<Object, ConflictId>( 256 );
+
+        {
+            ConflictId id = null;
+            Object key = conflictIds.get( node );
+            if ( key != null )
+            {
+                id = new ConflictId( key, 0 );
+                ids.put( key, id );
+            }
+
+            Map<DependencyNode, Object> visited = new IdentityHashMap<DependencyNode, Object>( conflictIds.size() );
+
+            buildConflitIdDAG( ids, node, id, 0, visited, conflictIds );
+        }
+
+        long time2 = System.nanoTime();
+
+        int cycles = topsortConflictIds( ids.values(), context );
+
+        if ( stats != null )
+        {
+            long time3 = System.nanoTime();
+            stats.put( "ConflictIdSorter.graphTime", time2 - time1 );
+            stats.put( "ConflictIdSorter.topsortTime", time3 - time2 );
+            stats.put( "ConflictIdSorter.conflictIdCount", ids.size() );
+            stats.put( "ConflictIdSorter.conflictIdCycleCount", cycles );
+        }
+
+        return node;
+    }
+
+    private void buildConflitIdDAG( Map<Object, ConflictId> ids, DependencyNode node, ConflictId id, int depth,
+                                    Map<DependencyNode, Object> visited, Map<?, ?> conflictIds )
+    {
+        if ( visited.put( node, Boolean.TRUE ) != null )
+        {
+            return;
+        }
+
+        depth++;
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            Object key = conflictIds.get( child );
+            ConflictId childId = ids.get( key );
+            if ( childId == null )
+            {
+                childId = new ConflictId( key, depth );
+                ids.put( key, childId );
+            }
+            else
+            {
+                childId.pullup( depth );
+            }
+
+            if ( id != null )
+            {
+                id.add( childId );
+            }
+
+            buildConflitIdDAG( ids, child, childId, depth, visited, conflictIds );
+        }
+    }
+
+    private int topsortConflictIds( Collection<ConflictId> conflictIds, DependencyGraphTransformationContext context )
+    {
+        List<Object> sorted = new ArrayList<Object>( conflictIds.size() );
+
+        RootQueue roots = new RootQueue( conflictIds.size() / 2 );
+        for ( ConflictId id : conflictIds )
+        {
+            if ( id.inDegree <= 0 )
+            {
+                roots.add( id );
+            }
+        }
+
+        processRoots( sorted, roots );
+
+        boolean cycle = sorted.size() < conflictIds.size();
+
+        while ( sorted.size() < conflictIds.size() )
+        {
+            // cycle -> deal gracefully with nodes still having positive in-degree
+
+            ConflictId nearest = null;
+            for ( ConflictId id : conflictIds )
+            {
+                if ( id.inDegree <= 0 )
+                {
+                    continue;
+                }
+                if ( nearest == null || id.minDepth < nearest.minDepth
+                    || ( id.minDepth == nearest.minDepth && id.inDegree < nearest.inDegree ) )
+                {
+                    nearest = id;
+                }
+            }
+
+            nearest.inDegree = 0;
+            roots.add( nearest );
+
+            processRoots( sorted, roots );
+        }
+
+        Collection<Collection<Object>> cycles = Collections.emptySet();
+        if ( cycle )
+        {
+            cycles = findCycles( conflictIds );
+        }
+
+        context.put( TransformationContextKeys.SORTED_CONFLICT_IDS, sorted );
+        context.put( TransformationContextKeys.CYCLIC_CONFLICT_IDS, cycles );
+
+        return cycles.size();
+    }
+
+    private void processRoots( List<Object> sorted, RootQueue roots )
+    {
+        while ( !roots.isEmpty() )
+        {
+            ConflictId root = roots.remove();
+
+            sorted.add( root.key );
+
+            for ( ConflictId child : root.children )
+            {
+                child.inDegree--;
+                if ( child.inDegree == 0 )
+                {
+                    roots.add( child );
+                }
+            }
+        }
+    }
+
+    private Collection<Collection<Object>> findCycles( Collection<ConflictId> conflictIds )
+    {
+        Collection<Collection<Object>> cycles = new HashSet<Collection<Object>>();
+
+        Map<Object, Integer> stack = new HashMap<Object, Integer>( 128 );
+        Map<ConflictId, Object> visited = new IdentityHashMap<ConflictId, Object>( conflictIds.size() );
+        for ( ConflictId id : conflictIds )
+        {
+            findCycles( id, visited, stack, cycles );
+        }
+
+        return cycles;
+    }
+
+    private void findCycles( ConflictId id, Map<ConflictId, Object> visited, Map<Object, Integer> stack,
+                             Collection<Collection<Object>> cycles )
+    {
+        Integer depth = stack.put( id.key, stack.size() );
+        if ( depth != null )
+        {
+            stack.put( id.key, depth );
+            Collection<Object> cycle = new HashSet<Object>();
+            for ( Map.Entry<Object, Integer> entry : stack.entrySet() )
+            {
+                if ( entry.getValue() >= depth )
+                {
+                    cycle.add( entry.getKey() );
+                }
+            }
+            cycles.add( cycle );
+        }
+        else
+        {
+            if ( visited.put( id, Boolean.TRUE ) == null )
+            {
+                for ( ConflictId childId : id.children )
+                {
+                    findCycles( childId, visited, stack, cycles );
+                }
+            }
+            stack.remove( id.key );
+        }
+    }
+
+    static final class ConflictId
+    {
+
+        final Object key;
+
+        Collection<ConflictId> children = Collections.emptySet();
+
+        int inDegree;
+
+        int minDepth;
+
+        public ConflictId( Object key, int depth )
+        {
+            this.key = key;
+            this.minDepth = depth;
+        }
+
+        public void add( ConflictId child )
+        {
+            if ( children.isEmpty() )
+            {
+                children = new HashSet<ConflictId>();
+            }
+            if ( children.add( child ) )
+            {
+                child.inDegree++;
+            }
+        }
+
+        public void pullup( int depth )
+        {
+            if ( depth < minDepth )
+            {
+                minDepth = depth;
+                depth++;
+                for ( ConflictId child : children )
+                {
+                    child.pullup( depth );
+                }
+            }
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof ConflictId ) )
+            {
+                return false;
+            }
+            ConflictId that = (ConflictId) obj;
+            return this.key.equals( that.key );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return key.hashCode();
+        }
+
+        @Override
+        public String toString()
+        {
+            return key + " @ " + minDepth + " <" + inDegree;
+        }
+
+    }
+
+    static final class RootQueue
+    {
+
+        private int nextOut;
+
+        private int nextIn;
+
+        private ConflictId[] ids;
+
+        RootQueue( int capacity )
+        {
+            ids = new ConflictId[capacity + 16];
+        }
+
+        boolean isEmpty()
+        {
+            return nextOut >= nextIn;
+        }
+
+        void add( ConflictId id )
+        {
+            if ( nextOut >= nextIn && nextOut > 0 )
+            {
+                nextIn -= nextOut;
+                nextOut = 0;
+            }
+            if ( nextIn >= ids.length )
+            {
+                ConflictId[] tmp = new ConflictId[ids.length + ids.length / 2 + 16];
+                System.arraycopy( ids, nextOut, tmp, 0, nextIn - nextOut );
+                ids = tmp;
+                nextIn -= nextOut;
+                nextOut = 0;
+            }
+            int i;
+            for ( i = nextIn - 1; i >= nextOut && id.minDepth < ids[i].minDepth; i-- )
+            {
+                ids[i + 1] = ids[i];
+            }
+            ids[i + 1] = id;
+            nextIn++;
+        }
+
+        ConflictId remove()
+        {
+            return ids[nextOut++];
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictMarker.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictMarker.java
new file mode 100644
index 0000000..fe2f5d5
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictMarker.java
@@ -0,0 +1,315 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency graph transformer that identifies conflicting dependencies. When this transformer has executed, the
+ * transformation context holds a {@code Map<DependencyNode, Object>} where dependency nodes that belong to the same
+ * conflict group will have an equal conflict identifier. This map is stored using the key
+ * {@link TransformationContextKeys#CONFLICT_IDS}.
+ */
+public final class ConflictMarker
+    implements DependencyGraphTransformer
+{
+
+    /**
+     * After the execution of this method, every DependencyNode with an attached dependency is member of one conflict
+     * group.
+     * 
+     * @see DependencyGraphTransformer#transformGraph(DependencyNode, DependencyGraphTransformationContext)
+     */
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        @SuppressWarnings( "unchecked" )
+        Map<String, Object> stats = (Map<String, Object>) context.get( TransformationContextKeys.STATS );
+        long time1 = System.nanoTime();
+
+        Map<DependencyNode, Object> nodes = new IdentityHashMap<DependencyNode, Object>( 1024 );
+        Map<Object, ConflictGroup> groups = new HashMap<Object, ConflictGroup>( 1024 );
+
+        analyze( node, nodes, groups, new int[] { 0 } );
+
+        long time2 = System.nanoTime();
+
+        Map<DependencyNode, Object> conflictIds = mark( nodes.keySet(), groups );
+
+        context.put( TransformationContextKeys.CONFLICT_IDS, conflictIds );
+
+        if ( stats != null )
+        {
+            long time3 = System.nanoTime();
+            stats.put( "ConflictMarker.analyzeTime", time2 - time1 );
+            stats.put( "ConflictMarker.markTime", time3 - time2 );
+            stats.put( "ConflictMarker.nodeCount", nodes.size() );
+        }
+
+        return node;
+    }
+
+    private void analyze( DependencyNode node, Map<DependencyNode, Object> nodes, Map<Object, ConflictGroup> groups,
+                          int[] counter )
+    {
+        if ( nodes.put( node, Boolean.TRUE ) != null )
+        {
+            return;
+        }
+
+        Set<Object> keys = getKeys( node );
+        if ( !keys.isEmpty() )
+        {
+            ConflictGroup group = null;
+            boolean fixMappings = false;
+
+            for ( Object key : keys )
+            {
+                ConflictGroup g = groups.get( key );
+
+                if ( group != g )
+                {
+                    if ( group == null )
+                    {
+                        Set<Object> newKeys = merge( g.keys, keys );
+                        if ( newKeys == g.keys )
+                        {
+                            group = g;
+                            break;
+                        }
+                        else
+                        {
+                            group = new ConflictGroup( newKeys, counter[0]++ );
+                            fixMappings = true;
+                        }
+                    }
+                    else if ( g == null )
+                    {
+                        fixMappings = true;
+                    }
+                    else
+                    {
+                        Set<Object> newKeys = merge( g.keys, group.keys );
+                        if ( newKeys == g.keys )
+                        {
+                            group = g;
+                            fixMappings = false;
+                            break;
+                        }
+                        else if ( newKeys != group.keys )
+                        {
+                            group = new ConflictGroup( newKeys, counter[0]++ );
+                            fixMappings = true;
+                        }
+                    }
+                }
+            }
+
+            if ( group == null )
+            {
+                group = new ConflictGroup( keys, counter[0]++ );
+                fixMappings = true;
+            }
+            if ( fixMappings )
+            {
+                for ( Object key : group.keys )
+                {
+                    groups.put( key, group );
+                }
+            }
+        }
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            analyze( child, nodes, groups, counter );
+        }
+    }
+
+    private Set<Object> merge( Set<Object> keys1, Set<Object> keys2 )
+    {
+        int size1 = keys1.size();
+        int size2 = keys2.size();
+
+        if ( size1 < size2 )
+        {
+            if ( keys2.containsAll( keys1 ) )
+            {
+                return keys2;
+            }
+        }
+        else
+        {
+            if ( keys1.containsAll( keys2 ) )
+            {
+                return keys1;
+            }
+        }
+
+        Set<Object> keys = new HashSet<Object>();
+        keys.addAll( keys1 );
+        keys.addAll( keys2 );
+        return keys;
+    }
+
+    private Set<Object> getKeys( DependencyNode node )
+    {
+        Set<Object> keys;
+
+        Dependency dependency = node.getDependency();
+
+        if ( dependency == null )
+        {
+            keys = Collections.emptySet();
+        }
+        else
+        {
+            Object key = toKey( dependency.getArtifact() );
+
+            if ( node.getRelocations().isEmpty() && node.getAliases().isEmpty() )
+            {
+                keys = Collections.singleton( key );
+            }
+            else
+            {
+                keys = new HashSet<Object>();
+                keys.add( key );
+
+                for ( Artifact relocation : node.getRelocations() )
+                {
+                    key = toKey( relocation );
+                    keys.add( key );
+                }
+
+                for ( Artifact alias : node.getAliases() )
+                {
+                    key = toKey( alias );
+                    keys.add( key );
+                }
+            }
+        }
+
+        return keys;
+    }
+
+    private Map<DependencyNode, Object> mark( Collection<DependencyNode> nodes, Map<Object, ConflictGroup> groups )
+    {
+        Map<DependencyNode, Object> conflictIds = new IdentityHashMap<DependencyNode, Object>( nodes.size() + 1 );
+
+        for ( DependencyNode node : nodes )
+        {
+            Dependency dependency = node.getDependency();
+            if ( dependency != null )
+            {
+                Object key = toKey( dependency.getArtifact() );
+                conflictIds.put( node, groups.get( key ).index );
+            }
+        }
+
+        return conflictIds;
+    }
+
+    private static Object toKey( Artifact artifact )
+    {
+        return new Key( artifact );
+    }
+
+    static class ConflictGroup
+    {
+
+        final Set<Object> keys;
+
+        final int index;
+
+        public ConflictGroup( Set<Object> keys, int index )
+        {
+            this.keys = keys;
+            this.index = index;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( keys );
+        }
+
+    }
+
+    static class Key
+    {
+
+        private final Artifact artifact;
+
+        public Key( Artifact artifact )
+        {
+            this.artifact = artifact;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( obj == this )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof Key ) )
+            {
+                return false;
+            }
+            Key that = (Key) obj;
+            return artifact.getArtifactId().equals( that.artifact.getArtifactId() )
+                && artifact.getGroupId().equals( that.artifact.getGroupId() )
+                && artifact.getExtension().equals( that.artifact.getExtension() )
+                && artifact.getClassifier().equals( that.artifact.getClassifier() );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            int hash = 17;
+            hash = hash * 31 + artifact.getArtifactId().hashCode();
+            hash = hash * 31 + artifact.getGroupId().hashCode();
+            hash = hash * 31 + artifact.getClassifier().hashCode();
+            hash = hash * 31 + artifact.getExtension().hashCode();
+            return hash;
+        }
+
+        @Override
+        public String toString()
+        {
+            return artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + artifact.getClassifier() + ':'
+                + artifact.getExtension();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java
new file mode 100644
index 0000000..7da2e48
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java
@@ -0,0 +1,1312 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * A dependency graph transformer that resolves version and scope conflicts among dependencies. For a given set of
+ * conflicting nodes, one node will be chosen as the winner and the other nodes are removed from the dependency graph.
+ * The exact rules by which a winning node and its effective scope are determined are controlled by user-supplied
+ * implementations of {@link VersionSelector}, {@link ScopeSelector}, {@link OptionalitySelector} and
+ * {@link ScopeDeriver}.
+ * <p>
+ * By default, this graph transformer will turn the dependency graph into a tree without duplicate artifacts. Using the
+ * configuration property {@link #CONFIG_PROP_VERBOSE}, a verbose mode can be enabled where the graph is still turned
+ * into a tree but all nodes participating in a conflict are retained. The nodes that were rejected during conflict
+ * resolution have no children and link back to the winner node via the {@link #NODE_DATA_WINNER} key in their custom
+ * data. Additionally, the keys {@link #NODE_DATA_ORIGINAL_SCOPE} and {@link #NODE_DATA_ORIGINAL_OPTIONALITY} are used
+ * to store the original scope and optionality of each node. Obviously, the resulting dependency tree is not suitable
+ * for artifact resolution unless a filter is employed to exclude the duplicate dependencies.
+ * <p>
+ * This transformer will query the keys {@link TransformationContextKeys#CONFLICT_IDS},
+ * {@link TransformationContextKeys#SORTED_CONFLICT_IDS}, {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} for
+ * existing information about conflict ids. In absence of this information, it will automatically invoke the
+ * {@link ConflictIdSorter} to calculate it.
+ */
+public final class ConflictResolver
+    implements DependencyGraphTransformer
+{
+
+    /**
+     * The key in the repository session's {@link RepositorySystemSession#getConfigProperties() configuration
+     * properties} used to store a {@link Boolean} flag controlling the transformer's verbose mode.
+     */
+    public static final String CONFIG_PROP_VERBOSE = "aether.conflictResolver.verbose";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which a reference to the
+     * {@link DependencyNode} which has won the conflict is stored.
+     */
+    public static final String NODE_DATA_WINNER = "conflict.winner";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the scope of the
+     * dependency before scope derivation and conflict resolution is stored.
+     */
+    public static final String NODE_DATA_ORIGINAL_SCOPE = "conflict.originalScope";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the optional flag of
+     * the dependency before derivation and conflict resolution is stored.
+     */
+    public static final String NODE_DATA_ORIGINAL_OPTIONALITY = "conflict.originalOptionality";
+
+    private final VersionSelector versionSelector;
+
+    private final ScopeSelector scopeSelector;
+
+    private final ScopeDeriver scopeDeriver;
+
+    private final OptionalitySelector optionalitySelector;
+
+    /**
+     * Creates a new conflict resolver instance with the specified hooks.
+     * 
+     * @param versionSelector The version selector to use, must not be {@code null}.
+     * @param scopeSelector The scope selector to use, must not be {@code null}.
+     * @param optionalitySelector The optionality selector ot use, must not be {@code null}.
+     * @param scopeDeriver The scope deriver to use, must not be {@code null}.
+     */
+    public ConflictResolver( VersionSelector versionSelector, ScopeSelector scopeSelector,
+                             OptionalitySelector optionalitySelector, ScopeDeriver scopeDeriver )
+    {
+        this.versionSelector = requireNonNull( versionSelector, "version selector cannot be null" );
+        this.scopeSelector = requireNonNull( scopeSelector, "scope selector cannot be null" );
+        this.optionalitySelector = requireNonNull( optionalitySelector, "optionality selector cannot be null" );
+        this.scopeDeriver = requireNonNull( scopeDeriver, "scope deriver cannot be null" );
+    }
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        List<?> sortedConflictIds = (List<?>) context.get( TransformationContextKeys.SORTED_CONFLICT_IDS );
+        if ( sortedConflictIds == null )
+        {
+            ConflictIdSorter sorter = new ConflictIdSorter();
+            sorter.transformGraph( node, context );
+
+            sortedConflictIds = (List<?>) context.get( TransformationContextKeys.SORTED_CONFLICT_IDS );
+        }
+
+        @SuppressWarnings( "unchecked" )
+        Map<String, Object> stats = (Map<String, Object>) context.get( TransformationContextKeys.STATS );
+        long time1 = System.nanoTime();
+
+        @SuppressWarnings( "unchecked" )
+        Collection<Collection<?>> conflictIdCycles =
+            (Collection<Collection<?>>) context.get( TransformationContextKeys.CYCLIC_CONFLICT_IDS );
+        if ( conflictIdCycles == null )
+        {
+            throw new RepositoryException( "conflict id cycles have not been identified" );
+        }
+
+        Map<?, ?> conflictIds = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        if ( conflictIds == null )
+        {
+            throw new RepositoryException( "conflict groups have not been identified" );
+        }
+
+        Map<Object, Collection<Object>> cyclicPredecessors = new HashMap<Object, Collection<Object>>();
+        for ( Collection<?> cycle : conflictIdCycles )
+        {
+            for ( Object conflictId : cycle )
+            {
+                Collection<Object> predecessors = cyclicPredecessors.get( conflictId );
+                if ( predecessors == null )
+                {
+                    predecessors = new HashSet<Object>();
+                    cyclicPredecessors.put( conflictId, predecessors );
+                }
+                predecessors.addAll( cycle );
+            }
+        }
+
+        State state = new State( node, conflictIds, sortedConflictIds.size(), context );
+        for ( Iterator<?> it = sortedConflictIds.iterator(); it.hasNext(); )
+        {
+            Object conflictId = it.next();
+
+            // reset data structures for next graph walk
+            state.prepare( conflictId, cyclicPredecessors.get( conflictId ) );
+
+            // find nodes with the current conflict id and while walking the graph (more deeply), nuke leftover losers
+            gatherConflictItems( node, state );
+
+            // now that we know the min depth of the parents, update depth of conflict items
+            state.finish();
+
+            // earlier runs might have nuked all parents of the current conflict id, so it might not exist anymore
+            if ( !state.items.isEmpty() )
+            {
+                ConflictContext ctx = state.conflictCtx;
+                state.versionSelector.selectVersion( ctx );
+                if ( ctx.winner == null )
+                {
+                    throw new RepositoryException( "conflict resolver did not select winner among " + state.items );
+                }
+                DependencyNode winner = ctx.winner.node;
+
+                state.scopeSelector.selectScope( ctx );
+                if ( state.verbose )
+                {
+                    winner.setData( NODE_DATA_ORIGINAL_SCOPE, winner.getDependency().getScope() );
+                }
+                winner.setScope( ctx.scope );
+
+                state.optionalitySelector.selectOptionality( ctx );
+                if ( state.verbose )
+                {
+                    winner.setData( NODE_DATA_ORIGINAL_OPTIONALITY, winner.getDependency().isOptional() );
+                }
+                winner.setOptional( ctx.optional );
+
+                removeLosers( state );
+            }
+
+            // record the winner so we can detect leftover losers during future graph walks
+            state.winner();
+
+            // in case of cycles, trigger final graph walk to ensure all leftover losers are gone
+            if ( !it.hasNext() && !conflictIdCycles.isEmpty() && state.conflictCtx.winner != null )
+            {
+                DependencyNode winner = state.conflictCtx.winner.node;
+                state.prepare( state, null );
+                gatherConflictItems( winner, state );
+            }
+        }
+
+        if ( stats != null )
+        {
+            long time2 = System.nanoTime();
+            stats.put( "ConflictResolver.totalTime", time2 - time1 );
+            stats.put( "ConflictResolver.conflictItemCount", state.totalConflictItems );
+        }
+
+        return node;
+    }
+
+    private boolean gatherConflictItems( DependencyNode node, State state )
+        throws RepositoryException
+    {
+        Object conflictId = state.conflictIds.get( node );
+        if ( state.currentId.equals( conflictId ) )
+        {
+            // found it, add conflict item (if not already done earlier by another path)
+            state.add( node );
+            // we don't recurse here so we might miss losers beneath us, those will be nuked during future walks below
+        }
+        else if ( state.loser( node, conflictId ) )
+        {
+            // found a leftover loser (likely in a cycle) of an already processed conflict id, tell caller to nuke it
+            return false;
+        }
+        else if ( state.push( node, conflictId ) )
+        {
+            // found potential parent, no cycle and not visisted before with the same derived scope, so recurse
+            for ( Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); )
+            {
+                DependencyNode child = it.next();
+                if ( !gatherConflictItems( child, state ) )
+                {
+                    it.remove();
+                }
+            }
+            state.pop();
+        }
+        return true;
+    }
+
+    private void removeLosers( State state )
+    {
+        ConflictItem winner = state.conflictCtx.winner;
+        List<DependencyNode> previousParent = null;
+        ListIterator<DependencyNode> childIt = null;
+        boolean conflictVisualized = false;
+        for ( ConflictItem item : state.items )
+        {
+            if ( item == winner )
+            {
+                continue;
+            }
+            if ( item.parent != previousParent )
+            {
+                childIt = item.parent.listIterator();
+                previousParent = item.parent;
+                conflictVisualized = false;
+            }
+            while ( childIt.hasNext() )
+            {
+                DependencyNode child = childIt.next();
+                if ( child == item.node )
+                {
+                    if ( state.verbose && !conflictVisualized && item.parent != winner.parent )
+                    {
+                        conflictVisualized = true;
+                        DependencyNode loser = new DefaultDependencyNode( child );
+                        loser.setData( NODE_DATA_WINNER, winner.node );
+                        loser.setData( NODE_DATA_ORIGINAL_SCOPE, loser.getDependency().getScope() );
+                        loser.setData( NODE_DATA_ORIGINAL_OPTIONALITY, loser.getDependency().isOptional() );
+                        loser.setScope( item.getScopes().iterator().next() );
+                        loser.setChildren( Collections.<DependencyNode>emptyList() );
+                        childIt.set( loser );
+                    }
+                    else
+                    {
+                        childIt.remove();
+                    }
+                    break;
+                }
+            }
+        }
+        // there might still be losers beneath the winner (e.g. in case of cycles)
+        // those will be nuked during future graph walks when we include the winner in the recursion
+    }
+
+    static final class NodeInfo
+    {
+
+        /**
+         * The smallest depth at which the node was seen, used for "the" depth of its conflict items.
+         */
+        int minDepth;
+
+        /**
+         * The set of derived scopes the node was visited with, used to check whether an already seen node needs to be
+         * revisited again in context of another scope. To conserve memory, we start with {@code String} and update to
+         * {@code Set<String>} if needed.
+         */
+        Object derivedScopes;
+
+        /**
+         * The set of derived optionalities the node was visited with, used to check whether an already seen node needs
+         * to be revisited again in context of another optionality. To conserve memory, encoded as bit field (bit 0 ->
+         * optional=false, bit 1 -> optional=true).
+         */
+        int derivedOptionalities;
+
+        /**
+         * The conflict items which are immediate children of the node, used to easily update those conflict items after
+         * a new parent scope/optionality was encountered.
+         */
+        List<ConflictItem> children;
+
+        static final int CHANGE_SCOPE = 0x01;
+
+        static final int CHANGE_OPTIONAL = 0x02;
+
+        private static final int OPT_FALSE = 0x01;
+
+        private static final int OPT_TRUE = 0x02;
+
+        NodeInfo( int depth, String derivedScope, boolean optional )
+        {
+            minDepth = depth;
+            derivedScopes = derivedScope;
+            derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE;
+        }
+
+        @SuppressWarnings( "unchecked" )
+        int update( int depth, String derivedScope, boolean optional )
+        {
+            if ( depth < minDepth )
+            {
+                minDepth = depth;
+            }
+            int changes;
+            if ( derivedScopes.equals( derivedScope ) )
+            {
+                changes = 0;
+            }
+            else if ( derivedScopes instanceof Collection )
+            {
+                changes = ( (Collection<String>) derivedScopes ).add( derivedScope ) ? CHANGE_SCOPE : 0;
+            }
+            else
+            {
+                Collection<String> scopes = new HashSet<String>();
+                scopes.add( (String) derivedScopes );
+                scopes.add( derivedScope );
+                derivedScopes = scopes;
+                changes = CHANGE_SCOPE;
+            }
+            int bit = optional ? OPT_TRUE : OPT_FALSE;
+            if ( ( derivedOptionalities & bit ) == 0 )
+            {
+                derivedOptionalities |= bit;
+                changes |= CHANGE_OPTIONAL;
+            }
+            return changes;
+        }
+
+        void add( ConflictItem item )
+        {
+            if ( children == null )
+            {
+                children = new ArrayList<ConflictItem>( 1 );
+            }
+            children.add( item );
+        }
+
+    }
+
+    final class State
+    {
+
+        /**
+         * The conflict id currently processed.
+         */
+        Object currentId;
+
+        /**
+         * Stats counter.
+         */
+        int totalConflictItems;
+
+        /**
+         * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts.
+         */
+        final boolean verbose;
+
+        /**
+         * A mapping from conflict id to winner node, helps to recognize nodes that have their effective
+         * scope&optionality set or are leftovers from previous removals.
+         */
+        final Map<Object, DependencyNode> resolvedIds;
+
+        /**
+         * The set of conflict ids which could apply to ancestors of nodes with the current conflict id, used to avoid
+         * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account
+         * for cyclic dependencies.
+         */
+        final Collection<Object> potentialAncestorIds;
+
+        /**
+         * The output from the conflict marker
+         */
+        final Map<?, ?> conflictIds;
+
+        /**
+         * The conflict items we have gathered so far for the current conflict id.
+         */
+        final List<ConflictItem> items;
+
+        /**
+         * The (conceptual) mapping from nodes to extra infos, technically keyed by the node's child list which better
+         * captures the identity of a node since we're basically concerned with effects towards children.
+         */
+        final Map<List<DependencyNode>, NodeInfo> infos;
+
+        /**
+         * The set of nodes on the DFS stack to detect cycles, technically keyed by the node's child list to match the
+         * dirty graph structure produced by the dependency collector for cycles.
+         */
+        final Map<List<DependencyNode>, Object> stack;
+
+        /**
+         * The stack of parent nodes.
+         */
+        final List<DependencyNode> parentNodes;
+
+        /**
+         * The stack of derived scopes for parent nodes.
+         */
+        final List<String> parentScopes;
+
+        /**
+         * The stack of derived optional flags for parent nodes.
+         */
+        final List<Boolean> parentOptionals;
+
+        /**
+         * The stack of node infos for parent nodes, may contain {@code null} which is used to disable creating new
+         * conflict items when visiting their parent again (conflict items are meant to be unique by parent-node combo).
+         */
+        final List<NodeInfo> parentInfos;
+
+        /**
+         * The conflict context passed to the version/scope/optionality selectors, updated as we move along rather than
+         * recreated to avoid tmp objects.
+         */
+        final ConflictContext conflictCtx;
+
+        /**
+         * The scope context passed to the scope deriver, updated as we move along rather than recreated to avoid tmp
+         * objects.
+         */
+        final ScopeContext scopeCtx;
+
+        /**
+         * The effective version selector, i.e. after initialization.
+         */
+        final VersionSelector versionSelector;
+
+        /**
+         * The effective scope selector, i.e. after initialization.
+         */
+        final ScopeSelector scopeSelector;
+
+        /**
+         * The effective scope deriver, i.e. after initialization.
+         */
+        final ScopeDeriver scopeDeriver;
+
+        /**
+         * The effective optionality selector, i.e. after initialization.
+         */
+        final OptionalitySelector optionalitySelector;
+
+        State( DependencyNode root, Map<?, ?> conflictIds, int conflictIdCount,
+               DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            this.conflictIds = conflictIds;
+            verbose = ConfigUtils.getBoolean( context.getSession(), false, CONFIG_PROP_VERBOSE );
+            potentialAncestorIds = new HashSet<Object>( conflictIdCount * 2 );
+            resolvedIds = new HashMap<Object, DependencyNode>( conflictIdCount * 2 );
+            items = new ArrayList<ConflictItem>( 256 );
+            infos = new IdentityHashMap<List<DependencyNode>, NodeInfo>( 64 );
+            stack = new IdentityHashMap<List<DependencyNode>, Object>( 64 );
+            parentNodes = new ArrayList<DependencyNode>( 64 );
+            parentScopes = new ArrayList<String>( 64 );
+            parentOptionals = new ArrayList<Boolean>( 64 );
+            parentInfos = new ArrayList<NodeInfo>( 64 );
+            conflictCtx = new ConflictContext( root, conflictIds, items );
+            scopeCtx = new ScopeContext( null, null );
+            versionSelector = ConflictResolver.this.versionSelector.getInstance( root, context );
+            scopeSelector = ConflictResolver.this.scopeSelector.getInstance( root, context );
+            scopeDeriver = ConflictResolver.this.scopeDeriver.getInstance( root, context );
+            optionalitySelector = ConflictResolver.this.optionalitySelector.getInstance( root, context );
+        }
+
+        void prepare( Object conflictId, Collection<Object> cyclicPredecessors )
+        {
+            currentId = conflictCtx.conflictId = conflictId;
+            conflictCtx.winner = null;
+            conflictCtx.scope = null;
+            conflictCtx.optional = null;
+            items.clear();
+            infos.clear();
+            if ( cyclicPredecessors != null )
+            {
+                potentialAncestorIds.addAll( cyclicPredecessors );
+            }
+        }
+
+        void finish()
+        {
+            List<DependencyNode> previousParent = null;
+            int previousDepth = 0;
+            totalConflictItems += items.size();
+            for ( int i = items.size() - 1; i >= 0; i-- )
+            {
+                ConflictItem item = items.get( i );
+                if ( item.parent == previousParent )
+                {
+                    item.depth = previousDepth;
+                }
+                else if ( item.parent != null )
+                {
+                    previousParent = item.parent;
+                    NodeInfo info = infos.get( previousParent );
+                    previousDepth = info.minDepth + 1;
+                    item.depth = previousDepth;
+                }
+            }
+            potentialAncestorIds.add( currentId );
+        }
+
+        void winner()
+        {
+            resolvedIds.put( currentId, ( conflictCtx.winner != null ) ? conflictCtx.winner.node : null );
+        }
+
+        boolean loser( DependencyNode node, Object conflictId )
+        {
+            DependencyNode winner = resolvedIds.get( conflictId );
+            return winner != null && winner != node;
+        }
+
+        boolean push( DependencyNode node, Object conflictId )
+            throws RepositoryException
+        {
+            if ( conflictId == null )
+            {
+                if ( node.getDependency() != null )
+                {
+                    if ( node.getData().get( NODE_DATA_WINNER ) != null )
+                    {
+                        return false;
+                    }
+                    throw new RepositoryException( "missing conflict id for node " + node );
+                }
+            }
+            else if ( !potentialAncestorIds.contains( conflictId ) )
+            {
+                return false;
+            }
+
+            List<DependencyNode> graphNode = node.getChildren();
+            if ( stack.put( graphNode, Boolean.TRUE ) != null )
+            {
+                return false;
+            }
+
+            int depth = depth();
+            String scope = deriveScope( node, conflictId );
+            boolean optional = deriveOptional( node, conflictId );
+            NodeInfo info = infos.get( graphNode );
+            if ( info == null )
+            {
+                info = new NodeInfo( depth, scope, optional );
+                infos.put( graphNode, info );
+                parentInfos.add( info );
+                parentNodes.add( node );
+                parentScopes.add( scope );
+                parentOptionals.add( optional );
+            }
+            else
+            {
+                int changes = info.update( depth, scope, optional );
+                if ( changes == 0 )
+                {
+                    stack.remove( graphNode );
+                    return false;
+                }
+                parentInfos.add( null ); // disable creating new conflict items, we update the existing ones below
+                parentNodes.add( node );
+                parentScopes.add( scope );
+                parentOptionals.add( optional );
+                if ( info.children != null )
+                {
+                    if ( ( changes & NodeInfo.CHANGE_SCOPE ) != 0 )
+                    {
+                        for ( int i = info.children.size() - 1; i >= 0; i-- )
+                        {
+                            ConflictItem item = info.children.get( i );
+                            String childScope = deriveScope( item.node, null );
+                            item.addScope( childScope );
+                        }
+                    }
+                    if ( ( changes & NodeInfo.CHANGE_OPTIONAL ) != 0 )
+                    {
+                        for ( int i = info.children.size() - 1; i >= 0; i-- )
+                        {
+                            ConflictItem item = info.children.get( i );
+                            boolean childOptional = deriveOptional( item.node, null );
+                            item.addOptional( childOptional );
+                        }
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        void pop()
+        {
+            int last = parentInfos.size() - 1;
+            parentInfos.remove( last );
+            parentScopes.remove( last );
+            parentOptionals.remove( last );
+            DependencyNode node = parentNodes.remove( last );
+            stack.remove( node.getChildren() );
+        }
+
+        void add( DependencyNode node )
+            throws RepositoryException
+        {
+            DependencyNode parent = parent();
+            if ( parent == null )
+            {
+                ConflictItem item = newConflictItem( parent, node );
+                items.add( item );
+            }
+            else
+            {
+                NodeInfo info = parentInfos.get( parentInfos.size() - 1 );
+                if ( info != null )
+                {
+                    ConflictItem item = newConflictItem( parent, node );
+                    info.add( item );
+                    items.add( item );
+                }
+            }
+        }
+
+        private ConflictItem newConflictItem( DependencyNode parent, DependencyNode node )
+            throws RepositoryException
+        {
+            return new ConflictItem( parent, node, deriveScope( node, null ), deriveOptional( node, null ) );
+        }
+
+        private int depth()
+        {
+            return parentNodes.size();
+        }
+
+        private DependencyNode parent()
+        {
+            int size = parentNodes.size();
+            return ( size <= 0 ) ? null : parentNodes.get( size - 1 );
+        }
+
+        private String deriveScope( DependencyNode node, Object conflictId )
+            throws RepositoryException
+        {
+            if ( ( node.getManagedBits() & DependencyNode.MANAGED_SCOPE ) != 0
+                || ( conflictId != null && resolvedIds.containsKey( conflictId ) ) )
+            {
+                return scope( node.getDependency() );
+            }
+
+            int depth = parentNodes.size();
+            scopes( depth, node.getDependency() );
+            if ( depth > 0 )
+            {
+                scopeDeriver.deriveScope( scopeCtx );
+            }
+            return scopeCtx.derivedScope;
+        }
+
+        private void scopes( int parent, Dependency child )
+        {
+            scopeCtx.parentScope = ( parent > 0 ) ? parentScopes.get( parent - 1 ) : null;
+            scopeCtx.derivedScope = scopeCtx.childScope = scope( child );
+        }
+
+        private String scope( Dependency dependency )
+        {
+            return ( dependency != null ) ? dependency.getScope() : null;
+        }
+
+        private boolean deriveOptional( DependencyNode node, Object conflictId )
+        {
+            Dependency dep = node.getDependency();
+            boolean optional = ( dep != null ) ? dep.isOptional() : false;
+            if ( optional || ( node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL ) != 0
+                || ( conflictId != null && resolvedIds.containsKey( conflictId ) ) )
+            {
+                return optional;
+            }
+            int depth = parentNodes.size();
+            return ( depth > 0 ) ? parentOptionals.get( depth - 1 ) : false;
+        }
+
+    }
+
+    /**
+     * A context used to hold information that is relevant for deriving the scope of a child dependency.
+     * 
+     * @see ScopeDeriver
+     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
+     *                change without notice and only exists to enable unit testing.
+     */
+    public static final class ScopeContext
+    {
+
+        String parentScope;
+
+        String childScope;
+
+        String derivedScope;
+
+        /**
+         * Creates a new scope context with the specified properties.
+         * 
+         * @param parentScope The scope of the parent dependency, may be {@code null}.
+         * @param childScope The scope of the child dependency, may be {@code null}.
+         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
+         *              change without notice and only exists to enable unit testing.
+         */
+        public ScopeContext( String parentScope, String childScope )
+        {
+            this.parentScope = ( parentScope != null ) ? parentScope : "";
+            derivedScope = this.childScope = ( childScope != null ) ? childScope : "";
+        }
+
+        /**
+         * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
+         * the scope deriver.
+         * 
+         * @return The scope of the parent dependency, never {@code null}.
+         */
+        public String getParentScope()
+        {
+            return parentScope;
+        }
+
+        /**
+         * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
+         * descriptor of the parent dependency.
+         * 
+         * @return The original scope of the child dependency, never {@code null}.
+         */
+        public String getChildScope()
+        {
+            return childScope;
+        }
+
+        /**
+         * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
+         * scope deriver makes changes.
+         * 
+         * @return The derived scope of the child dependency, never {@code null}.
+         */
+        public String getDerivedScope()
+        {
+            return derivedScope;
+        }
+
+        /**
+         * Sets the derived scope of the child dependency.
+         * 
+         * @param derivedScope The derived scope of the dependency, may be {@code null}.
+         */
+        public void setDerivedScope( String derivedScope )
+        {
+            this.derivedScope = ( derivedScope != null ) ? derivedScope : "";
+        }
+
+    }
+
+    /**
+     * A conflicting dependency.
+     * 
+     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
+     *                change without notice and only exists to enable unit testing.
+     */
+    public static final class ConflictItem
+    {
+
+        // nodes can share child lists, we care about the unique owner of a child node which is the child list
+        final List<DependencyNode> parent;
+
+        // only for debugging/toString() to help identify the parent node(s)
+        final Artifact artifact;
+
+        final DependencyNode node;
+
+        int depth;
+
+        // we start with String and update to Set<String> if needed
+        Object scopes;
+
+        // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE
+        int optionalities;
+
+        /**
+         * Bit flag indicating whether one or more paths consider the dependency non-optional.
+         */
+        public static final int OPTIONAL_FALSE = 0x01;
+
+        /**
+         * Bit flag indicating whether one or more paths consider the dependency optional.
+         */
+        public static final int OPTIONAL_TRUE = 0x02;
+
+        ConflictItem( DependencyNode parent, DependencyNode node, String scope, boolean optional )
+        {
+            if ( parent != null )
+            {
+                this.parent = parent.getChildren();
+                this.artifact = parent.getArtifact();
+            }
+            else
+            {
+                this.parent = null;
+                this.artifact = null;
+            }
+            this.node = node;
+            this.scopes = scope;
+            this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
+        }
+
+        /**
+         * Creates a new conflict item with the specified properties.
+         * 
+         * @param parent The parent node of the conflicting dependency, may be {@code null}.
+         * @param node The conflicting dependency, must not be {@code null}.
+         * @param depth The zero-based depth of the conflicting dependency.
+         * @param optionalities The optionalities the dependency was encountered with, encoded as a bit field consisting
+         *            of {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} and
+         *            {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE}.
+         * @param scopes The derived scopes of the conflicting dependency, must not be {@code null}.
+         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
+         *              change without notice and only exists to enable unit testing.
+         */
+        public ConflictItem( DependencyNode parent, DependencyNode node, int depth, int optionalities, String... scopes )
+        {
+            this.parent = ( parent != null ) ? parent.getChildren() : null;
+            this.artifact = ( parent != null ) ? parent.getArtifact() : null;
+            this.node = node;
+            this.depth = depth;
+            this.optionalities = optionalities;
+            this.scopes = Arrays.asList( scopes );
+        }
+
+        /**
+         * Determines whether the specified conflict item is a sibling of this item.
+         * 
+         * @param item The other conflict item, must not be {@code null}.
+         * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise.
+         */
+        public boolean isSibling( ConflictItem item )
+        {
+            return parent == item.parent;
+        }
+
+        /**
+         * Gets the dependency node involved in the conflict.
+         * 
+         * @return The involved dependency node, never {@code null}.
+         */
+        public DependencyNode getNode()
+        {
+            return node;
+        }
+
+        /**
+         * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
+         * 
+         * @return The involved dependency, never {@code null}.
+         */
+        public Dependency getDependency()
+        {
+            return node.getDependency();
+        }
+
+        /**
+         * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
+         * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
+         * possible depth.
+         * 
+         * @return The zero-based depth of the node in the graph.
+         */
+        public int getDepth()
+        {
+            return depth;
+        }
+
+        /**
+         * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
+         * different paths and each path might result in a different derived scope.
+         * 
+         * @see ScopeDeriver
+         * @return The (read-only) set of derived scopes of the dependency, never {@code null}.
+         */
+        @SuppressWarnings( "unchecked" )
+        public Collection<String> getScopes()
+        {
+            if ( scopes instanceof String )
+            {
+                return Collections.singleton( (String) scopes );
+            }
+            return (Collection<String>) scopes;
+        }
+
+        @SuppressWarnings( "unchecked" )
+        void addScope( String scope )
+        {
+            if ( scopes instanceof Collection )
+            {
+                ( (Collection<String>) scopes ).add( scope );
+            }
+            else if ( !scopes.equals( scope ) )
+            {
+                Collection<Object> set = new HashSet<Object>();
+                set.add( scopes );
+                set.add( scope );
+                scopes = set;
+            }
+        }
+
+        /**
+         * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
+         * different paths and each path might result in a different derived optionality.
+         * 
+         * @return A bit field consisting of {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
+         *         {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
+         *         dependency was encountered with.
+         */
+        public int getOptionalities()
+        {
+            return optionalities;
+        }
+
+        void addOptional( boolean optional )
+        {
+            optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
+        }
+
+        @Override
+        public String toString()
+        {
+            return node + " @ " + depth + " < " + artifact;
+        }
+
+    }
+
+    /**
+     * A context used to hold information that is relevant for resolving version and scope conflicts.
+     * 
+     * @see VersionSelector
+     * @see ScopeSelector
+     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
+     *                change without notice and only exists to enable unit testing.
+     */
+    public static final class ConflictContext
+    {
+
+        final DependencyNode root;
+
+        final Map<?, ?> conflictIds;
+
+        final Collection<ConflictItem> items;
+
+        Object conflictId;
+
+        ConflictItem winner;
+
+        String scope;
+
+        Boolean optional;
+
+        ConflictContext( DependencyNode root, Map<?, ?> conflictIds, Collection<ConflictItem> items )
+        {
+            this.root = root;
+            this.conflictIds = conflictIds;
+            this.items = Collections.unmodifiableCollection( items );
+        }
+
+        /**
+         * Creates a new conflict context.
+         * 
+         * @param root The root node of the dependency graph, must not be {@code null}.
+         * @param conflictId The conflict id for the set of conflicting dependencies in this context, must not be
+         *            {@code null}.
+         * @param conflictIds The mapping from dependency node to conflict id, must not be {@code null}.
+         * @param items The conflict items in this context, must not be {@code null}.
+         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
+         *              change without notice and only exists to enable unit testing.
+         */
+        public ConflictContext( DependencyNode root, Object conflictId, Map<DependencyNode, Object> conflictIds,
+                                Collection<ConflictItem> items )
+        {
+            this( root, conflictIds, items );
+            this.conflictId = conflictId;
+        }
+
+        /**
+         * Gets the root node of the dependency graph being transformed.
+         * 
+         * @return The root node of the dependeny graph, never {@code null}.
+         */
+        public DependencyNode getRoot()
+        {
+            return root;
+        }
+
+        /**
+         * Determines whether the specified dependency node belongs to this conflict context.
+         * 
+         * @param node The dependency node to check, must not be {@code null}.
+         * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise.
+         */
+        public boolean isIncluded( DependencyNode node )
+        {
+            return conflictId.equals( conflictIds.get( node ) );
+        }
+
+        /**
+         * Gets the collection of conflict items in this context.
+         * 
+         * @return The (read-only) collection of conflict items in this context, never {@code null}.
+         */
+        public Collection<ConflictItem> getItems()
+        {
+            return items;
+        }
+
+        /**
+         * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
+         * 
+         * @return The winning conflict item or {@code null} if not set yet.
+         */
+        public ConflictItem getWinner()
+        {
+            return winner;
+        }
+
+        /**
+         * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
+         * 
+         * @param winner The winning conflict item, may be {@code null}.
+         */
+        public void setWinner( ConflictItem winner )
+        {
+            this.winner = winner;
+        }
+
+        /**
+         * Gets the effective scope of the winning dependency.
+         * 
+         * @return The effective scope of the winning dependency or {@code null} if none.
+         */
+        public String getScope()
+        {
+            return scope;
+        }
+
+        /**
+         * Sets the effective scope of the winning dependency.
+         * 
+         * @param scope The effective scope, may be {@code null}.
+         */
+        public void setScope( String scope )
+        {
+            this.scope = scope;
+        }
+
+        /**
+         * Gets the effective optional flag of the winning dependency.
+         * 
+         * @return The effective optional flag or {@code null} if none.
+         */
+        public Boolean getOptional()
+        {
+            return optional;
+        }
+
+        /**
+         * Sets the effective optional flag of the winning dependency.
+         * 
+         * @param optional The effective optional flag, may be {@code null}.
+         */
+        public void setOptional( Boolean optional )
+        {
+            this.optional = optional;
+        }
+
+        @Override
+        public String toString()
+        {
+            return winner + " @ " + scope + " < " + items;
+        }
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the winner among conflicting dependencies. The
+     * winning node (and its children) will be retained in the dependency graph, the other nodes will get removed. The
+     * version selector does not need to deal with potential scope conflicts, these will be addressed afterwards by the
+     * {@link ScopeSelector}.
+     * <p>
+     * <strong>Note:</strong> Implementations must be stateless.
+     */
+    public abstract static class VersionSelector
+    {
+
+        /**
+         * Retrieves the version selector for use during the specified graph transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
+         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code null}.
+         * @return The scope deriver to use for the given graph transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public VersionSelector getInstance( DependencyNode root, DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the winning node among conflicting dependencies. Implementations will usually iterate
+         * {@link ConflictContext#getItems()}, inspect {@link ConflictItem#getNode()} and eventually call
+         * {@link ConflictContext#setWinner(ConflictResolver.ConflictItem)} to deliver the winner. Failure to select a
+         * winner will automatically fail the entire conflict resolution.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the version selection failed.
+         */
+        public abstract void selectVersion( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the effective scope of a dependency from a
+     * potentially conflicting set of {@link ScopeDeriver derived scopes}. The scope selector gets invoked after the
+     * {@link VersionSelector} has picked the winning node.
+     * <p>
+     * <strong>Note:</strong> Implementations must be stateless.
+     */
+    public abstract static class ScopeSelector
+    {
+
+        /**
+         * Retrieves the scope selector for use during the specified graph transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
+         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code null}.
+         * @return The scope selector to use for the given graph transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public ScopeSelector getInstance( DependencyNode root, DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the effective scope of the dependency given by {@link ConflictContext#getWinner()}.
+         * Implementations will usually iterate {@link ConflictContext#getItems()}, inspect
+         * {@link ConflictItem#getScopes()} and eventually call {@link ConflictContext#setScope(String)} to deliver the
+         * effective scope.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the scope selection failed.
+         */
+        public abstract void selectScope( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the scope of a dependency in relation to the scope
+     * of its parent.
+     * <p>
+     * <strong>Note:</strong> Implementations must be stateless.
+     */
+    public abstract static class ScopeDeriver
+    {
+
+        /**
+         * Retrieves the scope deriver for use during the specified graph transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
+         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code null}.
+         * @return The scope deriver to use for the given graph transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public ScopeDeriver getInstance( DependencyNode root, DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the scope of a dependency in relation to the scope of its parent. Implementors need to call
+         * {@link ScopeContext#setDerivedScope(String)} to deliver the result of their calculation. If said method is
+         * not invoked, the conflict resolver will assume the scope of the child dependency remains unchanged.
+         * 
+         * @param context The scope context, must not be {@code null}.
+         * @throws RepositoryException If the scope deriviation failed.
+         */
+        public abstract void deriveScope( ScopeContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the effective optional flag of a dependency from a
+     * potentially conflicting set of derived optionalities. The optionality selector gets invoked after the
+     * {@link VersionSelector} has picked the winning node.
+     * <p>
+     * <strong>Note:</strong> Implementations must be stateless.
+     */
+    public abstract static class OptionalitySelector
+    {
+
+        /**
+         * Retrieves the optionality selector for use during the specified graph transformation. The conflict resolver
+         * calls this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
+         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code null}.
+         * @return The optionality selector to use for the given graph transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public OptionalitySelector getInstance( DependencyNode root, DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the effective optional flag of the dependency given by {@link ConflictContext#getWinner()}.
+         * Implementations will usually iterate {@link ConflictContext#getItems()}, inspect
+         * {@link ConflictItem#getOptionalities()} and eventually call {@link ConflictContext#setOptional(Boolean)} to
+         * deliver the effective optional flag.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the optionality selection failed.
+         */
+        public abstract void selectOptionality( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefiner.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefiner.java
new file mode 100644
index 0000000..d96e04e
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefiner.java
@@ -0,0 +1,90 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.artifact.JavaScopes;
+
+/**
+ * A dependency graph transformer that refines the request context for nodes that belong to the "project" context by
+ * appending the classpath type to which the node belongs. For instance, a compile-time project dependency will be
+ * assigned the request context "project/compile".
+ * 
+ * @see DependencyNode#getRequestContext()
+ */
+public final class JavaDependencyContextRefiner
+    implements DependencyGraphTransformer
+{
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        String ctx = node.getRequestContext();
+
+        if ( "project".equals( ctx ) )
+        {
+            String scope = getClasspathScope( node );
+            if ( scope != null )
+            {
+                ctx += '/' + scope;
+                node.setRequestContext( ctx );
+            }
+        }
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            transformGraph( child, context );
+        }
+
+        return node;
+    }
+
+    private String getClasspathScope( DependencyNode node )
+    {
+        Dependency dependency = node.getDependency();
+        if ( dependency == null )
+        {
+            return null;
+        }
+
+        String scope = dependency.getScope();
+
+        if ( JavaScopes.COMPILE.equals( scope ) || JavaScopes.SYSTEM.equals( scope )
+            || JavaScopes.PROVIDED.equals( scope ) )
+        {
+            return JavaScopes.COMPILE;
+        }
+        else if ( JavaScopes.RUNTIME.equals( scope ) )
+        {
+            return JavaScopes.RUNTIME;
+        }
+        else if ( JavaScopes.TEST.equals( scope ) )
+        {
+            return JavaScopes.TEST;
+        }
+
+        return null;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeDeriver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeDeriver.java
new file mode 100644
index 0000000..4c5fd3e
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeDeriver.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.util.artifact.JavaScopes;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeContext;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeDeriver;
+
+/**
+ * A scope deriver for use with {@link ConflictResolver} that supports the scopes from {@link JavaScopes}.
+ */
+public final class JavaScopeDeriver
+    extends ScopeDeriver
+{
+
+    /**
+     * Creates a new instance of this scope deriver.
+     */
+    public JavaScopeDeriver()
+    {
+    }
+
+    @Override
+    public void deriveScope( ScopeContext context )
+        throws RepositoryException
+    {
+        context.setDerivedScope( getDerivedScope( context.getParentScope(), context.getChildScope() ) );
+    }
+
+    private String getDerivedScope( String parentScope, String childScope )
+    {
+        String derivedScope;
+
+        if ( JavaScopes.SYSTEM.equals( childScope ) || JavaScopes.TEST.equals( childScope ) )
+        {
+            derivedScope = childScope;
+        }
+        else if ( parentScope == null || parentScope.length() <= 0 || JavaScopes.COMPILE.equals( parentScope ) )
+        {
+            derivedScope = childScope;
+        }
+        else if ( JavaScopes.TEST.equals( parentScope ) || JavaScopes.RUNTIME.equals( parentScope ) )
+        {
+            derivedScope = parentScope;
+        }
+        else if ( JavaScopes.SYSTEM.equals( parentScope ) || JavaScopes.PROVIDED.equals( parentScope ) )
+        {
+            derivedScope = JavaScopes.PROVIDED;
+        }
+        else
+        {
+            derivedScope = JavaScopes.RUNTIME;
+        }
+
+        return derivedScope;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelector.java
new file mode 100644
index 0000000..93edf05
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelector.java
@@ -0,0 +1,107 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.util.artifact.JavaScopes;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ScopeSelector;
+
+/**
+ * A scope selector for use with {@link ConflictResolver} that supports the scopes from {@link JavaScopes}. In general,
+ * this selector picks the widest scope present among conflicting dependencies where e.g. "compile" is wider than
+ * "runtime" which is wider than "test". If however a direct dependency is involved, its scope is selected.
+ */
+public final class JavaScopeSelector
+    extends ScopeSelector
+{
+
+    /**
+     * Creates a new instance of this scope selector.
+     */
+    public JavaScopeSelector()
+    {
+    }
+
+    @Override
+    public void selectScope( ConflictContext context )
+        throws RepositoryException
+    {
+        String scope = context.getWinner().getDependency().getScope();
+        if ( !JavaScopes.SYSTEM.equals( scope ) )
+        {
+            scope = chooseEffectiveScope( context.getItems() );
+        }
+        context.setScope( scope );
+    }
+
+    private String chooseEffectiveScope( Collection<ConflictItem> items )
+    {
+        Set<String> scopes = new HashSet<String>();
+        for ( ConflictItem item : items )
+        {
+            if ( item.getDepth() <= 1 )
+            {
+                return item.getDependency().getScope();
+            }
+            scopes.addAll( item.getScopes() );
+        }
+        return chooseEffectiveScope( scopes );
+    }
+
+    private String chooseEffectiveScope( Set<String> scopes )
+    {
+        if ( scopes.size() > 1 )
+        {
+            scopes.remove( JavaScopes.SYSTEM );
+        }
+
+        String effectiveScope = "";
+
+        if ( scopes.size() == 1 )
+        {
+            effectiveScope = scopes.iterator().next();
+        }
+        else if ( scopes.contains( JavaScopes.COMPILE ) )
+        {
+            effectiveScope = JavaScopes.COMPILE;
+        }
+        else if ( scopes.contains( JavaScopes.RUNTIME ) )
+        {
+            effectiveScope = JavaScopes.RUNTIME;
+        }
+        else if ( scopes.contains( JavaScopes.PROVIDED ) )
+        {
+            effectiveScope = JavaScopes.PROVIDED;
+        }
+        else if ( scopes.contains( JavaScopes.TEST ) )
+        {
+            effectiveScope = JavaScopes.TEST;
+        }
+
+        return effectiveScope;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java
new file mode 100644
index 0000000..c1ffa85
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java
@@ -0,0 +1,185 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.UnsolvableVersionConflictException;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.VersionSelector;
+import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * A version selector for use with {@link ConflictResolver} that resolves version conflicts using a nearest-wins
+ * strategy. If there is no single node that satisfies all encountered version ranges, the selector will fail.
+ */
+public final class NearestVersionSelector
+    extends VersionSelector
+{
+
+    /**
+     * Creates a new instance of this version selector.
+     */
+    public NearestVersionSelector()
+    {
+    }
+
+    @Override
+    public void selectVersion( ConflictContext context )
+        throws RepositoryException
+    {
+        ConflictGroup group = new ConflictGroup();
+        for ( ConflictItem item : context.getItems() )
+        {
+            DependencyNode node = item.getNode();
+            VersionConstraint constraint = node.getVersionConstraint();
+
+            boolean backtrack = false;
+            boolean hardConstraint = constraint.getRange() != null;
+
+            if ( hardConstraint )
+            {
+                if ( group.constraints.add( constraint ) )
+                {
+                    if ( group.winner != null && !constraint.containsVersion( group.winner.getNode().getVersion() ) )
+                    {
+                        backtrack = true;
+                    }
+                }
+            }
+
+            if ( isAcceptable( group, node.getVersion() ) )
+            {
+                group.candidates.add( item );
+
+                if ( backtrack )
+                {
+                    backtrack( group, context );
+                }
+                else if ( group.winner == null || isNearer( item, group.winner ) )
+                {
+                    group.winner = item;
+                }
+            }
+            else if ( backtrack )
+            {
+                backtrack( group, context );
+            }
+        }
+        context.setWinner( group.winner );
+    }
+
+    private void backtrack( ConflictGroup group, ConflictContext context )
+        throws UnsolvableVersionConflictException
+    {
+        group.winner = null;
+
+        for ( Iterator<ConflictItem> it = group.candidates.iterator(); it.hasNext(); )
+        {
+            ConflictItem candidate = it.next();
+
+            if ( !isAcceptable( group, candidate.getNode().getVersion() ) )
+            {
+                it.remove();
+            }
+            else if ( group.winner == null || isNearer( candidate, group.winner ) )
+            {
+                group.winner = candidate;
+            }
+        }
+
+        if ( group.winner == null )
+        {
+            throw newFailure( context );
+        }
+    }
+
+    private boolean isAcceptable( ConflictGroup group, Version version )
+    {
+        for ( VersionConstraint constraint : group.constraints )
+        {
+            if ( !constraint.containsVersion( version ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isNearer( ConflictItem item1, ConflictItem item2 )
+    {
+        if ( item1.isSibling( item2 ) )
+        {
+            return item1.getNode().getVersion().compareTo( item2.getNode().getVersion() ) > 0;
+        }
+        else
+        {
+            return item1.getDepth() < item2.getDepth();
+        }
+    }
+
+    private UnsolvableVersionConflictException newFailure( final ConflictContext context )
+    {
+        DependencyFilter filter = new DependencyFilter()
+        {
+            public boolean accept( DependencyNode node, List<DependencyNode> parents )
+            {
+                return context.isIncluded( node );
+            }
+        };
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( filter );
+        context.getRoot().accept( visitor );
+        return new UnsolvableVersionConflictException( visitor.getPaths() );
+    }
+
+    static final class ConflictGroup
+    {
+
+        final Collection<VersionConstraint> constraints;
+
+        final Collection<ConflictItem> candidates;
+
+        ConflictItem winner;
+
+        public ConflictGroup()
+        {
+            constraints = new HashSet<VersionConstraint>();
+            candidates = new ArrayList<ConflictItem>( 64 );
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( winner );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NoopDependencyGraphTransformer.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NoopDependencyGraphTransformer.java
new file mode 100644
index 0000000..55b6175
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NoopDependencyGraphTransformer.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * A dependency graph transformer that does not perform any changes on its input.
+ */
+public final class NoopDependencyGraphTransformer
+    implements DependencyGraphTransformer
+{
+
+    /**
+     * A ready-made instance of this dependency graph transformer which can safely be reused throughout an entire
+     * application regardless of multi-threading.
+     */
+    public static final DependencyGraphTransformer INSTANCE = new NoopDependencyGraphTransformer();
+
+    /**
+     * Creates a new instance of this graph transformer. Usually, {@link #INSTANCE} should be used instead.
+     */
+    public NoopDependencyGraphTransformer()
+    {
+    }
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        return node;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelector.java
new file mode 100644
index 0000000..0cc7a73
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelector.java
@@ -0,0 +1,70 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.Collection;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver.OptionalitySelector;
+
+/**
+ * An optionality selector for use with {@link ConflictResolver}. In general, this selector only marks a dependency as
+ * optional if all its occurrences are optional. If however a direct dependency is involved, its optional flag is
+ * selected.
+ */
+public final class SimpleOptionalitySelector
+    extends OptionalitySelector
+{
+
+    /**
+     * Creates a new instance of this optionality selector.
+     */
+    public SimpleOptionalitySelector()
+    {
+    }
+
+    @Override
+    public void selectOptionality( ConflictContext context )
+        throws RepositoryException
+    {
+        boolean optional = chooseEffectiveOptionality( context.getItems() );
+        context.setOptional( optional );
+    }
+
+    private boolean chooseEffectiveOptionality( Collection<ConflictItem> items )
+    {
+        boolean optional = true;
+        for ( ConflictItem item : items )
+        {
+            if ( item.getDepth() <= 1 )
+            {
+                return item.getDependency().isOptional();
+            }
+            if ( ( item.getOptionalities() & ConflictItem.OPTIONAL_FALSE ) != 0 )
+            {
+                optional = false;
+            }
+        }
+        return optional;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/TransformationContextKeys.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/TransformationContextKeys.java
new file mode 100644
index 0000000..a9ebf68
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/TransformationContextKeys.java
@@ -0,0 +1,68 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+/**
+ * A collection of keys used by the dependency graph transformers when exchanging information via the graph
+ * transformation context.
+ * 
+ * @see org.eclipse.aether.collection.DependencyGraphTransformationContext#get(Object)
+ */
+public final class TransformationContextKeys
+{
+
+    /**
+     * The key in the graph transformation context where a {@code Map<DependencyNode, Object>} is stored which maps
+     * dependency nodes to their conflict ids. All nodes that map to an equal conflict id belong to the same group of
+     * conflicting dependencies. Note that the map keys use reference equality.
+     * 
+     * @see ConflictMarker
+     */
+    public static final Object CONFLICT_IDS = "conflictIds";
+
+    /**
+     * The key in the graph transformation context where a {@code List<Object>} is stored that denotes a topological
+     * sorting of the conflict ids.
+     * 
+     * @see ConflictIdSorter
+     */
+    public static final Object SORTED_CONFLICT_IDS = "sortedConflictIds";
+
+    /**
+     * The key in the graph transformation context where a {@code Collection<Collection<Object>>} is stored that denotes
+     * cycles among conflict ids. Each element in the outer collection denotes one cycle, i.e. if the collection is
+     * empty, the conflict ids have no cyclic dependencies.
+     * 
+     * @see ConflictIdSorter
+     */
+    public static final Object CYCLIC_CONFLICT_IDS = "cyclicConflictIds";
+
+    /**
+     * The key in the graph transformation context where a {@code Map<String, Object>} is stored that can be used to
+     * include some runtime/performance stats in the debug log. If this map is not present, no stats should be recorded.
+     */
+    public static final Object STATS = "stats";
+
+    private TransformationContextKeys()
+    {
+        // hide constructor
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/package-info.java
new file mode 100644
index 0000000..a41adcd
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency graph transformers for post-processing a dependency graph.
+ */
+package org.eclipse.aether.util.graph.transformer;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverser.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverser.java
new file mode 100644
index 0000000..fb08b3b
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverser.java
@@ -0,0 +1,207 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency traverser that combines zero or more other traversers using a logical {@code AND}. The resulting
+ * traverser enables processing of child dependencies if and only if all constituent traversers request traversal.
+ */
+public final class AndDependencyTraverser
+    implements DependencyTraverser
+{
+
+    private final Set<? extends DependencyTraverser> traversers;
+
+    private int hashCode;
+
+    /**
+     * Creates a new traverser from the specified traversers. Prefer
+     * {@link #newInstance(DependencyTraverser, DependencyTraverser)} if any of the input traversers might be
+     * {@code null}.
+     * 
+     * @param traversers The traversers to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencyTraverser( DependencyTraverser... traversers )
+    {
+        if ( traversers != null && traversers.length > 0 )
+        {
+            this.traversers = new LinkedHashSet<DependencyTraverser>( Arrays.asList( traversers ) );
+        }
+        else
+        {
+            this.traversers = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Creates a new traverser from the specified traversers.
+     * 
+     * @param traversers The traversers to combine, may be {@code null} but must not contain {@code null} elements.
+     */
+    public AndDependencyTraverser( Collection<? extends DependencyTraverser> traversers )
+    {
+        if ( traversers != null && !traversers.isEmpty() )
+        {
+            this.traversers = new LinkedHashSet<DependencyTraverser>( traversers );
+        }
+        else
+        {
+            this.traversers = Collections.emptySet();
+        }
+    }
+
+    private AndDependencyTraverser( Set<DependencyTraverser> traversers )
+    {
+        if ( traversers != null && !traversers.isEmpty() )
+        {
+            this.traversers = traversers;
+        }
+        else
+        {
+            this.traversers = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Creates a new traverser from the specified traversers.
+     * 
+     * @param traverser1 The first traverser to combine, may be {@code null}.
+     * @param traverser2 The second traverser to combine, may be {@code null}.
+     * @return The combined traverser or {@code null} if both traversers were {@code null}.
+     */
+    public static DependencyTraverser newInstance( DependencyTraverser traverser1, DependencyTraverser traverser2 )
+    {
+        if ( traverser1 == null )
+        {
+            return traverser2;
+        }
+        else if ( traverser2 == null || traverser2.equals( traverser1 ) )
+        {
+            return traverser1;
+        }
+        return new AndDependencyTraverser( traverser1, traverser2 );
+    }
+
+    public boolean traverseDependency( Dependency dependency )
+    {
+        for ( DependencyTraverser traverser : traversers )
+        {
+            if ( !traverser.traverseDependency( dependency ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public DependencyTraverser deriveChildTraverser( DependencyCollectionContext context )
+    {
+        int seen = 0;
+        Set<DependencyTraverser> childTraversers = null;
+
+        for ( DependencyTraverser traverser : traversers )
+        {
+            DependencyTraverser childTraverser = traverser.deriveChildTraverser( context );
+            if ( childTraversers != null )
+            {
+                if ( childTraverser != null )
+                {
+                    childTraversers.add( childTraverser );
+                }
+            }
+            else if ( traverser != childTraverser )
+            {
+                childTraversers = new LinkedHashSet<DependencyTraverser>();
+                if ( seen > 0 )
+                {
+                    for ( DependencyTraverser s : traversers )
+                    {
+                        if ( childTraversers.size() >= seen )
+                        {
+                            break;
+                        }
+                        childTraversers.add( s );
+                    }
+                }
+                if ( childTraverser != null )
+                {
+                    childTraversers.add( childTraverser );
+                }
+            }
+            else
+            {
+                seen++;
+            }
+        }
+
+        if ( childTraversers == null )
+        {
+            return this;
+        }
+        if ( childTraversers.size() <= 1 )
+        {
+            if ( childTraversers.isEmpty() )
+            {
+                return null;
+            }
+            return childTraversers.iterator().next();
+        }
+        return new AndDependencyTraverser( childTraversers );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        AndDependencyTraverser that = (AndDependencyTraverser) obj;
+        return traversers.equals( that.traversers );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = 17;
+            hash = hash * 31 + traversers.hashCode();
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverser.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverser.java
new file mode 100644
index 0000000..40ce616
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverser.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency traverser that excludes the dependencies of fat artifacts from the traversal. Fat artifacts are
+ * artifacts that have the property {@link org.eclipse.aether.artifact.ArtifactProperties#INCLUDES_DEPENDENCIES} set to
+ * {@code true}.
+ * 
+ * @see org.eclipse.aether.artifact.Artifact#getProperties()
+ */
+public final class FatArtifactTraverser
+    implements DependencyTraverser
+{
+
+    /**
+     * Creates a new instance of this dependency traverser.
+     */
+    public FatArtifactTraverser()
+    {
+    }
+
+    public boolean traverseDependency( Dependency dependency )
+    {
+        String prop = dependency.getArtifact().getProperty( ArtifactProperties.INCLUDES_DEPENDENCIES, "" );
+        return !Boolean.parseBoolean( prop );
+    }
+
+    public DependencyTraverser deriveChildTraverser( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverser.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverser.java
new file mode 100644
index 0000000..5e2a703
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverser.java
@@ -0,0 +1,79 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency traverser which always or never traverses children.
+ */
+public final class StaticDependencyTraverser
+    implements DependencyTraverser
+{
+
+    private final boolean traverse;
+
+    /**
+     * Creates a new traverser with the specified traversal behavior.
+     * 
+     * @param traverse {@code true} to traverse all dependencies, {@code false} to never traverse.
+     */
+    public StaticDependencyTraverser( boolean traverse )
+    {
+        this.traverse = traverse;
+    }
+
+    public boolean traverseDependency( Dependency dependency )
+    {
+        return traverse;
+    }
+
+    public DependencyTraverser deriveChildTraverser( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        StaticDependencyTraverser that = (StaticDependencyTraverser) obj;
+        return traverse == that.traverse;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = getClass().hashCode();
+        hash = hash * 31 + ( traverse ? 1 : 0 );
+        return hash;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/package-info.java
new file mode 100644
index 0000000..a1b71e0
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/traverser/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency traversers for building a dependency graph.
+ */
+package org.eclipse.aether.util.graph.traverser;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ChainedVersionFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ChainedVersionFilter.java
new file mode 100644
index 0000000..7d13555
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ChainedVersionFilter.java
@@ -0,0 +1,185 @@
+package org.eclipse.aether.util.graph.version;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.VersionFilter;
+
+/**
+ * A version filter that combines multiple version filters into a chain where each filter gets invoked one after the
+ * other, thereby accumulating their filtering effects.
+ */
+public final class ChainedVersionFilter
+    implements VersionFilter
+{
+
+    private final VersionFilter[] filters;
+
+    private int hashCode;
+
+    /**
+     * Chains the specified version filters.
+     * 
+     * @param filter1 The first version filter, may be {@code null}.
+     * @param filter2 The second version filter, may be {@code null}.
+     * @return The chained version filter or {@code null} if both input filters are {@code null}.
+     */
+    public static VersionFilter newInstance( VersionFilter filter1, VersionFilter filter2 )
+    {
+        if ( filter1 == null )
+        {
+            return filter2;
+        }
+        if ( filter2 == null )
+        {
+            return filter1;
+        }
+        return new ChainedVersionFilter( new VersionFilter[] { filter1, filter2 } );
+    }
+
+    /**
+     * Chains the specified version filters.
+     * 
+     * @param filters The version filters to chain, must not be {@code null} or contain {@code null}.
+     * @return The chained version filter or {@code null} if the input array is empty.
+     */
+    public static VersionFilter newInstance( VersionFilter... filters )
+    {
+        if ( filters.length <= 1 )
+        {
+            if ( filters.length <= 0 )
+            {
+                return null;
+            }
+            return filters[0];
+        }
+        return new ChainedVersionFilter( filters.clone() );
+    }
+
+    /**
+     * Chains the specified version filters.
+     * 
+     * @param filters The version filters to chain, must not be {@code null} or contain {@code null}.
+     * @return The chained version filter or {@code null} if the input collection is empty.
+     */
+    public static VersionFilter newInstance( Collection<? extends VersionFilter> filters )
+    {
+        if ( filters.size() <= 1 )
+        {
+            if ( filters.isEmpty() )
+            {
+                return null;
+            }
+            return filters.iterator().next();
+        }
+        return new ChainedVersionFilter( filters.toArray( new VersionFilter[filters.size()] ) );
+    }
+
+    private ChainedVersionFilter( VersionFilter[] filters )
+    {
+        this.filters = filters;
+    }
+
+    public void filterVersions( VersionFilterContext context )
+        throws RepositoryException
+    {
+        for ( int i = 0, n = filters.length; i < n && context.getCount() > 0; i++ )
+        {
+            filters[i].filterVersions( context );
+        }
+    }
+
+    public VersionFilter deriveChildFilter( DependencyCollectionContext context )
+    {
+        VersionFilter[] children = null;
+        int removed = 0;
+        for ( int i = 0, n = filters.length; i < n; i++ )
+        {
+            VersionFilter child = filters[i].deriveChildFilter( context );
+            if ( children != null )
+            {
+                children[i - removed] = child;
+            }
+            else if ( child != filters[i] )
+            {
+                children = new VersionFilter[filters.length];
+                System.arraycopy( filters, 0, children, 0, i );
+                children[i - removed] = child;
+            }
+            if ( child == null )
+            {
+                removed++;
+            }
+        }
+        if ( children == null )
+        {
+            return this;
+        }
+        if ( removed > 0 )
+        {
+            int count = filters.length - removed;
+            if ( count <= 0 )
+            {
+                return null;
+            }
+            if ( count == 1 )
+            {
+                return children[0];
+            }
+            VersionFilter[] tmp = new VersionFilter[count];
+            System.arraycopy( children, 0, tmp, 0, count );
+            children = tmp;
+        }
+        return new ChainedVersionFilter( children );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ChainedVersionFilter that = (ChainedVersionFilter) obj;
+        return Arrays.equals( filters, that.filters );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = getClass().hashCode();
+            hash = hash * 31 + Arrays.hashCode( filters );
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ContextualSnapshotVersionFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ContextualSnapshotVersionFilter.java
new file mode 100644
index 0000000..569bf4c
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/ContextualSnapshotVersionFilter.java
@@ -0,0 +1,109 @@
+package org.eclipse.aether.util.graph.version;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * A version filter that blocks "*-SNAPSHOT" versions if the
+ * {@link org.eclipse.aether.collection.CollectRequest#getRootArtifact() root artifact} of the dependency graph is not a
+ * snapshot. Alternatively, this filter can be forced to always ban snapshot versions by setting the boolean
+ * {@link RepositorySystemSession#getConfigProperties() configuration property} {@link #CONFIG_PROP_ENABLE} to
+ * {@code true}.
+ */
+public final class ContextualSnapshotVersionFilter
+    implements VersionFilter
+{
+
+    /**
+     * The key in the repository session's {@link RepositorySystemSession#getConfigProperties() configuration
+     * properties} used to store a {@link Boolean} flag whether this filter should be forced to ban snapshots. By
+     * default, snapshots are only filtered if the root artifact is not a snapshot.
+     */
+    public static final String CONFIG_PROP_ENABLE = "aether.snapshotFilter";
+
+    private final SnapshotVersionFilter filter;
+
+    /**
+     * Creates a new instance of this version filter.
+     */
+    public ContextualSnapshotVersionFilter()
+    {
+        filter = new SnapshotVersionFilter();
+    }
+
+    private boolean isEnabled( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean( session, false, CONFIG_PROP_ENABLE );
+    }
+
+    public void filterVersions( VersionFilterContext context )
+    {
+        if ( isEnabled( context.getSession() ) )
+        {
+            filter.filterVersions( context );
+        }
+    }
+
+    public VersionFilter deriveChildFilter( DependencyCollectionContext context )
+    {
+        if ( !isEnabled( context.getSession() ) )
+        {
+            Artifact artifact = context.getArtifact();
+            if ( artifact == null )
+            {
+                // no root artifact to test, allow snapshots and recheck once we reach the direct dependencies
+                return this;
+            }
+            if ( artifact.isSnapshot() )
+            {
+                // root is a snapshot, allow snapshots all the way down
+                return null;
+            }
+        }
+        // artifact is a non-snapshot or filter explicitly enabled, block snapshots all the way down
+        return filter;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/HighestVersionFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/HighestVersionFilter.java
new file mode 100644
index 0000000..902e08d
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/HighestVersionFilter.java
@@ -0,0 +1,80 @@
+package org.eclipse.aether.util.graph.version;
+
+/*
+ * 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.
+ */
+
+import java.util.Iterator;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.version.Version;
+
+/**
+ * A version filter that excludes any version except the highest one.
+ */
+public final class HighestVersionFilter
+    implements VersionFilter
+{
+
+    /**
+     * Creates a new instance of this version filter.
+     */
+    public HighestVersionFilter()
+    {
+    }
+
+    public void filterVersions( VersionFilterContext context )
+    {
+        Iterator<Version> it = context.iterator();
+        for ( boolean hasNext = it.hasNext(); hasNext; )
+        {
+            it.next();
+            if ( hasNext = it.hasNext() )
+            {
+                it.remove();
+            }
+        }
+    }
+
+    public VersionFilter deriveChildFilter( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/SnapshotVersionFilter.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/SnapshotVersionFilter.java
new file mode 100644
index 0000000..6af7cf5
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/SnapshotVersionFilter.java
@@ -0,0 +1,80 @@
+package org.eclipse.aether.util.graph.version;
+
+/*
+ * 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.
+ */
+
+import java.util.Iterator;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.version.Version;
+
+/**
+ * A version filter that (unconditionally) blocks "*-SNAPSHOT" versions. For practical purposes,
+ * {@link ContextualSnapshotVersionFilter} is usually more desirable.
+ */
+public final class SnapshotVersionFilter
+    implements VersionFilter
+{
+
+    /**
+     * Creates a new instance of this version filter.
+     */
+    public SnapshotVersionFilter()
+    {
+    }
+
+    public void filterVersions( VersionFilterContext context )
+    {
+        for ( Iterator<Version> it = context.iterator(); it.hasNext(); )
+        {
+            String version = it.next().toString();
+            if ( version.endsWith( "SNAPSHOT" ) )
+            {
+                it.remove();
+            }
+        }
+    }
+
+    public VersionFilter deriveChildFilter( DependencyCollectionContext context )
+    {
+        return this;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/package-info.java
new file mode 100644
index 0000000..a9f4649
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/version/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various version filters for building a dependency graph.
+ */
+package org.eclipse.aether.util.graph.version;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java
new file mode 100644
index 0000000..70fc7d4
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java
@@ -0,0 +1,186 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstract base class for depth first dependency tree traversers. Subclasses of this visitor will visit each node
+ * exactly once regardless how many paths within the dependency graph lead to the node such that the resulting node
+ * sequence is free of duplicates.
+ * <p>
+ * Actual vertex ordering (preorder, inorder, postorder) needs to be defined by subclasses through appropriate
+ * implementations for {@link #visitEnter(org.eclipse.aether.graph.DependencyNode)} and
+ * {@link #visitLeave(org.eclipse.aether.graph.DependencyNode)}
+ */
+abstract class AbstractDepthFirstNodeListGenerator
+    implements DependencyVisitor
+{
+
+    private final Map<DependencyNode, Object> visitedNodes;
+
+    protected final List<DependencyNode> nodes;
+
+    public AbstractDepthFirstNodeListGenerator()
+    {
+        nodes = new ArrayList<DependencyNode>( 128 );
+        visitedNodes = new IdentityHashMap<DependencyNode, Object>( 512 );
+    }
+
+    /**
+     * Gets the list of dependency nodes that was generated during the graph traversal.
+     * 
+     * @return The list of dependency nodes, never {@code null}.
+     */
+    public List<DependencyNode> getNodes()
+    {
+        return nodes;
+    }
+
+    /**
+     * Gets the dependencies seen during the graph traversal.
+     * 
+     * @param includeUnresolved Whether unresolved dependencies shall be included in the result or not.
+     * @return The list of dependencies, never {@code null}.
+     */
+    public List<Dependency> getDependencies( boolean includeUnresolved )
+    {
+        List<Dependency> dependencies = new ArrayList<Dependency>( getNodes().size() );
+
+        for ( DependencyNode node : getNodes() )
+        {
+            Dependency dependency = node.getDependency();
+            if ( dependency != null )
+            {
+                if ( includeUnresolved || dependency.getArtifact().getFile() != null )
+                {
+                    dependencies.add( dependency );
+                }
+            }
+        }
+
+        return dependencies;
+    }
+
+    /**
+     * Gets the artifacts associated with the list of dependency nodes generated during the graph traversal.
+     * 
+     * @param includeUnresolved Whether unresolved artifacts shall be included in the result or not.
+     * @return The list of artifacts, never {@code null}.
+     */
+    public List<Artifact> getArtifacts( boolean includeUnresolved )
+    {
+        List<Artifact> artifacts = new ArrayList<Artifact>( getNodes().size() );
+
+        for ( DependencyNode node : getNodes() )
+        {
+            if ( node.getDependency() != null )
+            {
+                Artifact artifact = node.getDependency().getArtifact();
+                if ( includeUnresolved || artifact.getFile() != null )
+                {
+                    artifacts.add( artifact );
+                }
+            }
+        }
+
+        return artifacts;
+    }
+
+    /**
+     * Gets the files of resolved artifacts seen during the graph traversal.
+     * 
+     * @return The list of artifact files, never {@code null}.
+     */
+    public List<File> getFiles()
+    {
+        List<File> files = new ArrayList<File>( getNodes().size() );
+
+        for ( DependencyNode node : getNodes() )
+        {
+            if ( node.getDependency() != null )
+            {
+                File file = node.getDependency().getArtifact().getFile();
+                if ( file != null )
+                {
+                    files.add( file );
+                }
+            }
+        }
+
+        return files;
+    }
+
+    /**
+     * Gets a class path by concatenating the artifact files of the visited dependency nodes. Nodes with unresolved
+     * artifacts are automatically skipped.
+     * 
+     * @return The class path, using the platform-specific path separator, never {@code null}.
+     */
+    public String getClassPath()
+    {
+        StringBuilder buffer = new StringBuilder( 1024 );
+
+        for ( Iterator<DependencyNode> it = getNodes().iterator(); it.hasNext(); )
+        {
+            DependencyNode node = it.next();
+            if ( node.getDependency() != null )
+            {
+                Artifact artifact = node.getDependency().getArtifact();
+                if ( artifact.getFile() != null )
+                {
+                    buffer.append( artifact.getFile().getAbsolutePath() );
+                    if ( it.hasNext() )
+                    {
+                        buffer.append( File.pathSeparatorChar );
+                    }
+                }
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    /**
+     * Marks the specified node as being visited and determines whether the node has been visited before.
+     * 
+     * @param node The node being visited, must not be {@code null}.
+     * @return {@code true} if the node has not been visited before, {@code false} if the node was already visited.
+     */
+    protected boolean setVisited( DependencyNode node )
+    {
+        return visitedNodes.put( node, Boolean.TRUE ) == null;
+    }
+
+    public abstract boolean visitEnter( DependencyNode node );
+
+    public abstract boolean visitLeave( DependencyNode node );
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java
new file mode 100644
index 0000000..a39fc84
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java
@@ -0,0 +1,114 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+
+/**
+ * A dependency visitor that constructs a clone of the visited dependency graph. If such a visitor is passed into a
+ * {@link FilteringDependencyVisitor}, a sub graph can be created. This class creates shallow clones of the visited
+ * dependency nodes (via {@link DefaultDependencyNode#DefaultDependencyNode(DependencyNode)}) but clients can create a
+ * subclass and override {@link #clone(DependencyNode)} to alter the clone process.
+ */
+public class CloningDependencyVisitor
+    implements DependencyVisitor
+{
+
+    private final Map<DependencyNode, DependencyNode> clones;
+
+    private final Stack<DependencyNode> parents;
+
+    private DependencyNode root;
+
+    /**
+     * Creates a new visitor that clones the visited nodes.
+     */
+    public CloningDependencyVisitor()
+    {
+        parents = new Stack<DependencyNode>();
+        clones = new IdentityHashMap<DependencyNode, DependencyNode>( 256 );
+    }
+
+    /**
+     * Gets the root node of the cloned dependency graph.
+     * 
+     * @return The root node of the cloned dependency graph or {@code null}.
+     */
+    public final DependencyNode getRootNode()
+    {
+        return root;
+    }
+
+    /**
+     * Creates a clone of the specified node.
+     * 
+     * @param node The node to clone, must not be {@code null}.
+     * @return The cloned node, never {@code null}.
+     */
+    protected DependencyNode clone( DependencyNode node )
+    {
+        DefaultDependencyNode clone = new DefaultDependencyNode( node );
+        return clone;
+    }
+
+    public final boolean visitEnter( DependencyNode node )
+    {
+        boolean recurse = true;
+
+        DependencyNode clone = clones.get( node );
+        if ( clone == null )
+        {
+            clone = clone( node );
+            clones.put( node, clone );
+        }
+        else
+        {
+            recurse = false;
+        }
+
+        DependencyNode parent = parents.peek();
+
+        if ( parent == null )
+        {
+            root = clone;
+        }
+        else
+        {
+            parent.getChildren().add( clone );
+        }
+
+        parents.push( clone );
+
+        return recurse;
+    }
+
+    public final boolean visitLeave( DependencyNode node )
+    {
+        parents.pop();
+
+        return true;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java
new file mode 100644
index 0000000..a126bb7
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java
@@ -0,0 +1,112 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+
+/**
+ * A dependency visitor that delegates to another visitor if nodes match a filter. Note that in case of a mismatching
+ * node, the children of that node are still visisted and presented to the filter.
+ */
+public final class FilteringDependencyVisitor
+    implements DependencyVisitor
+{
+
+    private final DependencyFilter filter;
+
+    private final DependencyVisitor visitor;
+
+    private final Stack<Boolean> accepts;
+
+    private final Stack<DependencyNode> parents;
+
+    /**
+     * Creates a new visitor that delegates traversal of nodes matching the given filter to the specified visitor.
+     *
+     * @param visitor The visitor to delegate to, must not be {@code null}.
+     * @param filter The filter to apply, may be {@code null} to not filter.
+     */
+    public FilteringDependencyVisitor( DependencyVisitor visitor, DependencyFilter filter )
+    {
+        this.visitor = requireNonNull( visitor, "dependency visitor delegate cannot be null" );
+        this.filter = filter;
+        this.accepts = new Stack<Boolean>();
+        this.parents = new Stack<DependencyNode>();
+    }
+
+    /**
+     * Gets the visitor to which this visitor delegates to.
+     * 
+     * @return The visitor being delegated to, never {@code null}.
+     */
+    public DependencyVisitor getVisitor()
+    {
+        return visitor;
+    }
+
+    /**
+     * Gets the filter being applied before delegation.
+     * 
+     * @return The filter being applied or {@code null} if none.
+     */
+    public DependencyFilter getFilter()
+    {
+        return filter;
+    }
+
+    public boolean visitEnter( DependencyNode node )
+    {
+        boolean accept = filter == null || filter.accept( node, parents );
+
+        accepts.push( accept );
+
+        parents.push( node );
+
+        if ( accept )
+        {
+            return visitor.visitEnter( node );
+        }
+        else
+        {
+            return true;
+        }
+    }
+
+    public boolean visitLeave( DependencyNode node )
+    {
+        parents.pop();
+
+        Boolean accept = accepts.pop();
+
+        if ( accept )
+        {
+            return visitor.visitLeave( node );
+        }
+        else
+        {
+            return true;
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java
new file mode 100644
index 0000000..d1814ed
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java
@@ -0,0 +1,137 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+
+/**
+ * A dependency visitor that records all paths leading to nodes matching a certain filter criteria.
+ */
+public final class PathRecordingDependencyVisitor
+    implements DependencyVisitor
+{
+
+    private final DependencyFilter filter;
+
+    private final List<List<DependencyNode>> paths;
+
+    private final Stack<DependencyNode> parents;
+
+    private final Map<DependencyNode, Object> visited;
+
+    private final boolean excludeChildrenOfMatches;
+
+    /**
+     * Creates a new visitor that uses the specified filter to identify terminal nodes of interesting paths. The visitor
+     * will not search for paths going beyond an already matched node.
+     * 
+     * @param filter The filter used to select terminal nodes of paths to record, may be {@code null} to match any node.
+     */
+    public PathRecordingDependencyVisitor( DependencyFilter filter )
+    {
+        this( filter, true );
+    }
+
+    /**
+     * Creates a new visitor that uses the specified filter to identify terminal nodes of interesting paths.
+     * 
+     * @param filter The filter used to select terminal nodes of paths to record, may be {@code null} to match any node.
+     * @param excludeChildrenOfMatches Flag controlling whether children of matched nodes should be excluded from the
+     *            traversal, thereby ignoring any potential paths to other matching nodes beneath a matching ancestor
+     *            node. If {@code true}, all recorded paths will have only one matching node (namely the terminal node),
+     *            if {@code false} a recorded path can consist of multiple matching nodes.
+     */
+    public PathRecordingDependencyVisitor( DependencyFilter filter, boolean excludeChildrenOfMatches )
+    {
+        this.filter = filter;
+        this.excludeChildrenOfMatches = excludeChildrenOfMatches;
+        paths = new ArrayList<List<DependencyNode>>();
+        parents = new Stack<DependencyNode>();
+        visited = new IdentityHashMap<DependencyNode, Object>( 128 );
+    }
+
+    /**
+     * Gets the filter being used to select terminal nodes.
+     * 
+     * @return The filter being used or {@code null} if none.
+     */
+    public DependencyFilter getFilter()
+    {
+        return filter;
+    }
+
+    /**
+     * Gets the paths leading to nodes matching the filter that have been recorded during the graph visit. A path is
+     * given as a sequence of nodes, starting with the root node of the graph and ending with a node that matched the
+     * filter.
+     * 
+     * @return The recorded paths, never {@code null}.
+     */
+    public List<List<DependencyNode>> getPaths()
+    {
+        return paths;
+    }
+
+    public boolean visitEnter( DependencyNode node )
+    {
+        boolean accept = filter == null || filter.accept( node, parents );
+
+        parents.push( node );
+
+        if ( accept )
+        {
+            DependencyNode[] path = new DependencyNode[parents.size()];
+            for ( int i = 0, n = parents.size(); i < n; i++ )
+            {
+                path[n - i - 1] = parents.get( i );
+            }
+            paths.add( Arrays.asList( path ) );
+
+            if ( excludeChildrenOfMatches )
+            {
+                return false;
+            }
+        }
+
+        if ( visited.put( node, Boolean.TRUE ) != null )
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    public boolean visitLeave( DependencyNode node )
+    {
+        parents.pop();
+        visited.remove( node );
+
+        return true;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java
new file mode 100644
index 0000000..47897a7
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Generates a sequence of dependency nodes from a dependeny graph by traversing the graph in postorder. This visitor
+ * visits each node exactly once regardless how many paths within the dependency graph lead to the node such that the
+ * resulting node sequence is free of duplicates.
+ */
+public final class PostorderNodeListGenerator
+    extends AbstractDepthFirstNodeListGenerator
+{
+
+    private final Stack<Boolean> visits;
+
+    /**
+     * Creates a new postorder list generator.
+     */
+    public PostorderNodeListGenerator()
+    {
+        visits = new Stack<Boolean>();
+    }
+
+    @Override
+    public boolean visitEnter( DependencyNode node )
+    {
+        boolean visited = !setVisited( node );
+
+        visits.push( visited );
+
+        if ( visited )
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean visitLeave( DependencyNode node )
+    {
+        Boolean visited = visits.pop();
+
+        if ( visited )
+        {
+            return true;
+        }
+
+        if ( node.getDependency() != null )
+        {
+            nodes.add( node );
+        }
+
+        return true;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java
new file mode 100644
index 0000000..bd9b52a
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java
@@ -0,0 +1,62 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Generates a sequence of dependency nodes from a dependeny graph by traversing the graph in preorder. This visitor
+ * visits each node exactly once regardless how many paths within the dependency graph lead to the node such that the
+ * resulting node sequence is free of duplicates.
+ */
+public final class PreorderNodeListGenerator
+    extends AbstractDepthFirstNodeListGenerator
+{
+
+    /**
+     * Creates a new preorder list generator.
+     */
+    public PreorderNodeListGenerator()
+    {
+    }
+
+    @Override
+    public boolean visitEnter( DependencyNode node )
+    {
+        if ( !setVisited( node ) )
+        {
+            return false;
+        }
+
+        if ( node.getDependency() != null )
+        {
+            nodes.add( node );
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean visitLeave( DependencyNode node )
+    {
+        return true;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/Stack.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/Stack.java
new file mode 100644
index 0000000..27fbb4b
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/Stack.java
@@ -0,0 +1,86 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import java.util.AbstractList;
+import java.util.NoSuchElementException;
+import java.util.RandomAccess;
+
+/**
+ * A non-synchronized stack with a non-modifiable list view which starts at the top of the stack. While
+ * {@code LinkedList} can provide the same behavior, it creates many temp objects upon frequent pushes/pops.
+ */
+class Stack<E>
+    extends AbstractList<E>
+    implements RandomAccess
+{
+
+    @SuppressWarnings( "unchecked" )
+    private E[] elements = (E[]) new Object[96];
+
+    private int size;
+
+    public void push( E element )
+    {
+        if ( size >= elements.length )
+        {
+            @SuppressWarnings( "unchecked" )
+            E[] tmp = (E[]) new Object[size + 64];
+            System.arraycopy( elements, 0, tmp, 0, elements.length );
+            elements = tmp;
+        }
+        elements[size++] = element;
+    }
+
+    public E pop()
+    {
+        if ( size <= 0 )
+        {
+            throw new NoSuchElementException();
+        }
+        return elements[--size];
+    }
+
+    public E peek()
+    {
+        if ( size <= 0 )
+        {
+            return null;
+        }
+        return elements[size - 1];
+    }
+
+    @Override
+    public E get( int index )
+    {
+        if ( index < 0 || index >= size )
+        {
+            throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size );
+        }
+        return elements[size - index - 1];
+    }
+
+    @Override
+    public int size()
+    {
+        return size;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java
new file mode 100644
index 0000000..2f9012d
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+
+/**
+ * A dependency visitor that delegates to another visitor if a node hasn't been visited before. In other words, this
+ * visitor provides a tree-view of a dependency graph which generally can have multiple paths to the same node or even
+ * cycles.
+ */
+public final class TreeDependencyVisitor
+    implements DependencyVisitor
+{
+
+    private final Map<DependencyNode, Object> visitedNodes;
+
+    private final DependencyVisitor visitor;
+
+    private final Stack<Boolean> visits;
+
+    /**
+     * Creates a new visitor that delegates to the specified visitor.
+     *
+     * @param visitor The visitor to delegate to, must not be {@code null}.
+     */
+    public TreeDependencyVisitor( DependencyVisitor visitor )
+    {
+        this.visitor = requireNonNull( visitor, "dependency visitor delegate cannot be null" );
+        visitedNodes = new IdentityHashMap<DependencyNode, Object>( 512 );
+        visits = new Stack<Boolean>();
+    }
+
+    public boolean visitEnter( DependencyNode node )
+    {
+        boolean visited = visitedNodes.put( node, Boolean.TRUE ) != null;
+
+        visits.push( visited );
+
+        if ( visited )
+        {
+            return false;
+        }
+
+        return visitor.visitEnter( node );
+    }
+
+    public boolean visitLeave( DependencyNode node )
+    {
+        Boolean visited = visits.pop();
+
+        if ( visited )
+        {
+            return true;
+        }
+
+        return visitor.visitLeave( node );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/package-info.java
new file mode 100644
index 0000000..3ea9968
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Various dependency visitors for inspecting a dependency graph.
+ */
+package org.eclipse.aether.util.graph.visitor;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedRepositoryListener.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedRepositoryListener.java
new file mode 100644
index 0000000..c654510
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedRepositoryListener.java
@@ -0,0 +1,437 @@
+package org.eclipse.aether.util.listener;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.aether.AbstractRepositoryListener;
+import org.eclipse.aether.RepositoryEvent;
+import org.eclipse.aether.RepositoryListener;
+
+/**
+ * A repository listener that delegates to zero or more other listeners (multicast). The list of target listeners is
+ * thread-safe, i.e. target listeners can be added or removed by any thread at any time.
+ */
+public final class ChainedRepositoryListener
+    extends AbstractRepositoryListener
+{
+
+    private final List<RepositoryListener> listeners = new CopyOnWriteArrayList<RepositoryListener>();
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners. In contrast to the constructor, this
+     * factory method will avoid creating an actual chained listener if one of the specified readers is actually
+     * {@code null}.
+     * 
+     * @param listener1 The first listener, may be {@code null}.
+     * @param listener2 The second listener, may be {@code null}.
+     * @return The chained listener or {@code null} if no listener was supplied.
+     */
+    public static RepositoryListener newInstance( RepositoryListener listener1, RepositoryListener listener2 )
+    {
+        if ( listener1 == null )
+        {
+            return listener2;
+        }
+        else if ( listener2 == null )
+        {
+            return listener1;
+        }
+        return new ChainedRepositoryListener( listener1, listener2 );
+    }
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners.
+     * 
+     * @param listeners The listeners to delegate to, may be {@code null} or empty.
+     */
+    public ChainedRepositoryListener( RepositoryListener... listeners )
+    {
+        if ( listeners != null )
+        {
+            add( Arrays.asList( listeners ) );
+        }
+    }
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners.
+     * 
+     * @param listeners The listeners to delegate to, may be {@code null} or empty.
+     */
+    public ChainedRepositoryListener( Collection<? extends RepositoryListener> listeners )
+    {
+        add( listeners );
+    }
+
+    /**
+     * Adds the specified listeners to the end of the multicast chain.
+     * 
+     * @param listeners The listeners to add, may be {@code null} or empty.
+     */
+    public void add( Collection<? extends RepositoryListener> listeners )
+    {
+        if ( listeners != null )
+        {
+            for ( RepositoryListener listener : listeners )
+            {
+                add( listener );
+            }
+        }
+    }
+
+    /**
+     * Adds the specified listener to the end of the multicast chain.
+     * 
+     * @param listener The listener to add, may be {@code null}.
+     */
+    public void add( RepositoryListener listener )
+    {
+        if ( listener != null )
+        {
+            listeners.add( listener );
+        }
+    }
+
+    /**
+     * Removes the specified listener from the multicast chain. Trying to remove a non-existing listener has no effect.
+     * 
+     * @param listener The listener to remove, may be {@code null}.
+     */
+    public void remove( RepositoryListener listener )
+    {
+        if ( listener != null )
+        {
+            listeners.remove( listener );
+        }
+    }
+
+    protected void handleError( RepositoryEvent event, RepositoryListener listener, RuntimeException error )
+    {
+        // default just swallows errors
+    }
+
+    @Override
+    public void artifactDeployed( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDeployed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactDeploying( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDeploying( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactDescriptorInvalid( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDescriptorInvalid( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactDescriptorMissing( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDescriptorMissing( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactDownloaded( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDownloaded( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactDownloading( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactDownloading( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactInstalled( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactInstalled( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactInstalling( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactInstalling( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactResolved( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactResolved( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void artifactResolving( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.artifactResolving( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataDeployed( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataDeployed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataDeploying( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataDeploying( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataDownloaded( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataDownloaded( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataDownloading( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataDownloading( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataInstalled( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataInstalled( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataInstalling( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataInstalling( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataInvalid( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataInvalid( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataResolved( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataResolved( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void metadataResolving( RepositoryEvent event )
+    {
+        for ( RepositoryListener listener : listeners )
+        {
+            try
+            {
+                listener.metadataResolving( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedTransferListener.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedTransferListener.java
new file mode 100644
index 0000000..d943105
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/ChainedTransferListener.java
@@ -0,0 +1,234 @@
+package org.eclipse.aether.util.listener;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transfer.TransferCancelledException;
+import org.eclipse.aether.transfer.TransferEvent;
+import org.eclipse.aether.transfer.TransferListener;
+
+/**
+ * A transfer listener that delegates to zero or more other listeners (multicast). The list of target listeners is
+ * thread-safe, i.e. target listeners can be added or removed by any thread at any time.
+ */
+public final class ChainedTransferListener
+    extends AbstractTransferListener
+{
+
+    private final List<TransferListener> listeners = new CopyOnWriteArrayList<TransferListener>();
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners. In contrast to the constructor, this
+     * factory method will avoid creating an actual chained listener if one of the specified readers is actually
+     * {@code null}.
+     * 
+     * @param listener1 The first listener, may be {@code null}.
+     * @param listener2 The second listener, may be {@code null}.
+     * @return The chained listener or {@code null} if no listener was supplied.
+     */
+    public static TransferListener newInstance( TransferListener listener1, TransferListener listener2 )
+    {
+        if ( listener1 == null )
+        {
+            return listener2;
+        }
+        else if ( listener2 == null )
+        {
+            return listener1;
+        }
+        return new ChainedTransferListener( listener1, listener2 );
+    }
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners.
+     * 
+     * @param listeners The listeners to delegate to, may be {@code null} or empty.
+     */
+    public ChainedTransferListener( TransferListener... listeners )
+    {
+        if ( listeners != null )
+        {
+            add( Arrays.asList( listeners ) );
+        }
+    }
+
+    /**
+     * Creates a new multicast listener that delegates to the specified listeners.
+     * 
+     * @param listeners The listeners to delegate to, may be {@code null} or empty.
+     */
+    public ChainedTransferListener( Collection<? extends TransferListener> listeners )
+    {
+        add( listeners );
+    }
+
+    /**
+     * Adds the specified listeners to the end of the multicast chain.
+     * 
+     * @param listeners The listeners to add, may be {@code null} or empty.
+     */
+    public void add( Collection<? extends TransferListener> listeners )
+    {
+        if ( listeners != null )
+        {
+            for ( TransferListener listener : listeners )
+            {
+                add( listener );
+            }
+        }
+    }
+
+    /**
+     * Adds the specified listener to the end of the multicast chain.
+     * 
+     * @param listener The listener to add, may be {@code null}.
+     */
+    public void add( TransferListener listener )
+    {
+        if ( listener != null )
+        {
+            listeners.add( listener );
+        }
+    }
+
+    /**
+     * Removes the specified listener from the multicast chain. Trying to remove a non-existing listener has no effect.
+     * 
+     * @param listener The listener to remove, may be {@code null}.
+     */
+    public void remove( TransferListener listener )
+    {
+        if ( listener != null )
+        {
+            listeners.remove( listener );
+        }
+    }
+
+    protected void handleError( TransferEvent event, TransferListener listener, RuntimeException error )
+    {
+        // default just swallows errors
+    }
+
+    @Override
+    public void transferInitiated( TransferEvent event )
+        throws TransferCancelledException
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferInitiated( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferStarted( TransferEvent event )
+        throws TransferCancelledException
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferStarted( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferProgressed( TransferEvent event )
+        throws TransferCancelledException
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferProgressed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferCorrupted( TransferEvent event )
+        throws TransferCancelledException
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferCorrupted( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferSucceeded( TransferEvent event )
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferSucceeded( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+    @Override
+    public void transferFailed( TransferEvent event )
+    {
+        for ( TransferListener listener : listeners )
+        {
+            try
+            {
+                listener.transferFailed( event );
+            }
+            catch ( RuntimeException e )
+            {
+                handleError( event, listener, e );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/package-info.java
new file mode 100644
index 0000000..9f0be58
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/listener/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Utilities to build repository and transfer listeners.
+ */
+package org.eclipse.aether.util.listener;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/package-info.java
new file mode 100644
index 0000000..605e777
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Miscellaneous utility classes.
+ */
+package org.eclipse.aether.util;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/AuthenticationBuilder.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/AuthenticationBuilder.java
new file mode 100644
index 0000000..bc69e85
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/AuthenticationBuilder.java
@@ -0,0 +1,231 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.net.ssl.HostnameVerifier;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+
+/**
+ * A utility class to build authentication info for repositories and proxies.
+ */
+public final class AuthenticationBuilder
+{
+
+    private final List<Authentication> authentications;
+
+    /**
+     * Creates a new authentication builder.
+     */
+    public AuthenticationBuilder()
+    {
+        authentications = new ArrayList<Authentication>();
+    }
+
+    /**
+     * Builds a new authentication object from the current data of this builder. The state of the builder itself remains
+     * unchanged.
+     * 
+     * @return The authentication or {@code null} if no authentication data was supplied to the builder.
+     */
+    public Authentication build()
+    {
+        if ( authentications.isEmpty() )
+        {
+            return null;
+        }
+        if ( authentications.size() == 1 )
+        {
+            return authentications.get( 0 );
+        }
+        return new ChainedAuthentication( authentications );
+    }
+
+    /**
+     * Adds username data to the authentication.
+     * 
+     * @param username The username, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addUsername( String username )
+    {
+        return addString( AuthenticationContext.USERNAME, username );
+    }
+
+    /**
+     * Adds password data to the authentication.
+     * 
+     * @param password The password, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addPassword( String password )
+    {
+        return addSecret( AuthenticationContext.PASSWORD, password );
+    }
+
+    /**
+     * Adds password data to the authentication. The resulting authentication object uses an encrypted copy of the
+     * supplied character data and callers are advised to clear the input array soon after this method returns.
+     * 
+     * @param password The password, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addPassword( char[] password )
+    {
+        return addSecret( AuthenticationContext.PASSWORD, password );
+    }
+
+    /**
+     * Adds NTLM data to the authentication.
+     * 
+     * @param workstation The NTLM workstation name, may be {@code null}.
+     * @param domain The NTLM domain name, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addNtlm( String workstation, String domain )
+    {
+        addString( AuthenticationContext.NTLM_WORKSTATION, workstation );
+        return addString( AuthenticationContext.NTLM_DOMAIN, domain );
+    }
+
+    /**
+     * Adds private key data to the authentication.
+     * 
+     * @param pathname The (absolute) path to the private key file, may be {@code null}.
+     * @param passphrase The passphrase protecting the private key, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addPrivateKey( String pathname, String passphrase )
+    {
+        if ( pathname != null )
+        {
+            addString( AuthenticationContext.PRIVATE_KEY_PATH, pathname );
+            addSecret( AuthenticationContext.PRIVATE_KEY_PASSPHRASE, passphrase );
+        }
+        return this;
+    }
+
+    /**
+     * Adds private key data to the authentication. The resulting authentication object uses an encrypted copy of the
+     * supplied character data and callers are advised to clear the input array soon after this method returns.
+     * 
+     * @param pathname The (absolute) path to the private key file, may be {@code null}.
+     * @param passphrase The passphrase protecting the private key, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addPrivateKey( String pathname, char[] passphrase )
+    {
+        if ( pathname != null )
+        {
+            addString( AuthenticationContext.PRIVATE_KEY_PATH, pathname );
+            addSecret( AuthenticationContext.PRIVATE_KEY_PASSPHRASE, passphrase );
+        }
+        return this;
+    }
+
+    /**
+     * Adds a hostname verifier for SSL. <strong>Note:</strong> This method assumes that all possible instances of the
+     * verifier's runtime type exhibit the exact same behavior, i.e. the behavior of the verifier depends solely on the
+     * runtime type and not on any configuration. For verifiers that do not fit this assumption, use
+     * {@link #addCustom(Authentication)} with a suitable implementation instead.
+     * 
+     * @param verifier The hostname verifier, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addHostnameVerifier( HostnameVerifier verifier )
+    {
+        if ( verifier != null )
+        {
+            authentications.add( new ComponentAuthentication( AuthenticationContext.SSL_HOSTNAME_VERIFIER, verifier ) );
+        }
+        return this;
+    }
+
+    /**
+     * Adds custom string data to the authentication. <em>Note:</em> If the string data is confidential, use
+     * {@link #addSecret(String, char[])} instead.
+     * 
+     * @param key The key for the authentication data, must not be {@code null}.
+     * @param value The value for the authentication data, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addString( String key, String value )
+    {
+        if ( value != null )
+        {
+            authentications.add( new StringAuthentication( key, value ) );
+        }
+        return this;
+    }
+
+    /**
+     * Adds sensitive custom string data to the authentication.
+     * 
+     * @param key The key for the authentication data, must not be {@code null}.
+     * @param value The value for the authentication data, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addSecret( String key, String value )
+    {
+        if ( value != null )
+        {
+            authentications.add( new SecretAuthentication( key, value ) );
+        }
+        return this;
+    }
+
+    /**
+     * Adds sensitive custom string data to the authentication. The resulting authentication object uses an encrypted
+     * copy of the supplied character data and callers are advised to clear the input array soon after this method
+     * returns.
+     * 
+     * @param key The key for the authentication data, must not be {@code null}.
+     * @param value The value for the authentication data, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addSecret( String key, char[] value )
+    {
+        if ( value != null )
+        {
+            authentications.add( new SecretAuthentication( key, value ) );
+        }
+        return this;
+    }
+
+    /**
+     * Adds custom authentication data to the authentication.
+     * 
+     * @param authentication The authentication to add, may be {@code null}.
+     * @return This builder for chaining, never {@code null}.
+     */
+    public AuthenticationBuilder addCustom( Authentication authentication )
+    {
+        if ( authentication != null )
+        {
+            authentications.add( authentication );
+        }
+        return this;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedAuthentication.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedAuthentication.java
new file mode 100644
index 0000000..40dae3f
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedAuthentication.java
@@ -0,0 +1,116 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+
+/**
+ * Authentication that aggregates other authentication blocks. When multiple input authentication blocks provide the
+ * same authentication key, the last written value wins.
+ */
+final class ChainedAuthentication
+    implements Authentication
+{
+
+    private final Authentication[] authentications;
+
+    public ChainedAuthentication( Authentication... authentications )
+    {
+        if ( authentications != null && authentications.length > 0 )
+        {
+            this.authentications = authentications.clone();
+        }
+        else
+        {
+            this.authentications = new Authentication[0];
+        }
+    }
+
+    public ChainedAuthentication( Collection<? extends Authentication> authentications )
+    {
+        if ( authentications != null && !authentications.isEmpty() )
+        {
+            this.authentications = authentications.toArray( new Authentication[authentications.size()] );
+        }
+        else
+        {
+            this.authentications = new Authentication[0];
+        }
+    }
+
+    public void fill( AuthenticationContext context, String key, Map<String, String> data )
+    {
+        for ( Authentication authentication : authentications )
+        {
+            authentication.fill( context, key, data );
+        }
+    }
+
+    public void digest( AuthenticationDigest digest )
+    {
+        for ( Authentication authentication : authentications )
+        {
+            authentication.digest( digest );
+        }
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        ChainedAuthentication that = (ChainedAuthentication) obj;
+        return Arrays.equals( authentications, that.authentications );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Arrays.hashCode( authentications );
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 256 );
+        for ( Authentication authentication : authentications )
+        {
+            if ( buffer.length() > 0 )
+            {
+                buffer.append( ", " );
+            }
+            buffer.append( authentication );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
new file mode 100644
index 0000000..0a9b8f6
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedWorkspaceReader.java
@@ -0,0 +1,164 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.WorkspaceReader;
+import org.eclipse.aether.repository.WorkspaceRepository;
+
+/**
+ * A workspace reader that delegates to a chain of other readers, effectively aggregating their contents.
+ */
+public final class ChainedWorkspaceReader
+    implements WorkspaceReader
+{
+
+    private List<WorkspaceReader> readers = new ArrayList<WorkspaceReader>();
+
+    private WorkspaceRepository repository;
+
+    /**
+     * Creates a new workspace reader by chaining the specified readers.
+     * 
+     * @param readers The readers to chain, may be {@code null}.
+     * @see #newInstance(WorkspaceReader, WorkspaceReader)
+     */
+    public ChainedWorkspaceReader( WorkspaceReader... readers )
+    {
+        if ( readers != null )
+        {
+            Collections.addAll( this.readers, readers );
+        }
+
+        StringBuilder buffer = new StringBuilder();
+        for ( WorkspaceReader reader : this.readers )
+        {
+            if ( buffer.length() > 0 )
+            {
+                buffer.append( '+' );
+            }
+            buffer.append( reader.getRepository().getContentType() );
+        }
+
+        repository = new WorkspaceRepository( buffer.toString(), new Key( this.readers ) );
+    }
+
+    /**
+     * Creates a new workspace reader by chaining the specified readers. In contrast to the constructor, this factory
+     * method will avoid creating an actual chained reader if one of the specified readers is actually {@code null}.
+     * 
+     * @param reader1 The first workspace reader, may be {@code null}.
+     * @param reader2 The second workspace reader, may be {@code null}.
+     * @return The chained reader or {@code null} if no workspace reader was supplied.
+     */
+    public static WorkspaceReader newInstance( WorkspaceReader reader1, WorkspaceReader reader2 )
+    {
+        if ( reader1 == null )
+        {
+            return reader2;
+        }
+        else if ( reader2 == null )
+        {
+            return reader1;
+        }
+        return new ChainedWorkspaceReader( reader1, reader2 );
+    }
+
+    public File findArtifact( Artifact artifact )
+    {
+        File file = null;
+
+        for ( WorkspaceReader reader : readers )
+        {
+            file = reader.findArtifact( artifact );
+            if ( file != null )
+            {
+                break;
+            }
+        }
+
+        return file;
+    }
+
+    public List<String> findVersions( Artifact artifact )
+    {
+        Collection<String> versions = new LinkedHashSet<String>();
+
+        for ( WorkspaceReader reader : readers )
+        {
+            versions.addAll( reader.findVersions( artifact ) );
+        }
+
+        return Collections.unmodifiableList( new ArrayList<String>( versions ) );
+    }
+
+    public WorkspaceRepository getRepository()
+    {
+        Key key = new Key( readers );
+        if ( !key.equals( repository.getKey() ) )
+        {
+            repository = new WorkspaceRepository( repository.getContentType(), key );
+        }
+        return repository;
+    }
+
+    private static class Key
+    {
+
+        private final List<Object> keys = new ArrayList<Object>();
+
+        public Key( List<WorkspaceReader> readers )
+        {
+            for ( WorkspaceReader reader : readers )
+            {
+                keys.add( reader.getRepository().getKey() );
+            }
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            if ( obj == null || !getClass().equals( obj.getClass() ) )
+            {
+                return false;
+            }
+            return keys.equals( ( (Key) obj ).keys );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return keys.hashCode();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ComponentAuthentication.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ComponentAuthentication.java
new file mode 100644
index 0000000..c9c206a
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ComponentAuthentication.java
@@ -0,0 +1,99 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+
+/**
+ * Authentication block that manages a single authentication key and its component value. In this context, component
+ * refers to an object whose behavior is solely dependent on its implementation class.
+ */
+final class ComponentAuthentication
+    implements Authentication
+{
+
+    private final String key;
+
+    private final Object value;
+
+    public ComponentAuthentication( String key, Object value )
+    {
+        this.key = requireNonNull( key, "authentication key cannot be null" );
+        if ( key.length() == 0 )
+        {
+            throw new IllegalArgumentException( "authentication key cannot be empty" );
+        }
+        this.value = value;
+    }
+
+    public void fill( AuthenticationContext context, String key, Map<String, String> data )
+    {
+        context.put( this.key, value );
+    }
+
+    public void digest( AuthenticationDigest digest )
+    {
+        if ( value != null )
+        {
+            digest.update( key, value.getClass().getName() );
+        }
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        ComponentAuthentication that = (ComponentAuthentication) obj;
+        return key.equals( that.key ) && eqClass( value, that.value );
+    }
+
+    private static <T> boolean eqClass( T s1, T s2 )
+    {
+        return ( s1 == null ) ? s2 == null : s2 != null && s1.getClass().equals( s2.getClass() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + key.hashCode();
+        hash = hash * 31 + ( ( value != null ) ? value.getClass().hashCode() : 0 );
+        return hash;
+    }
+
+    @Override
+    public String toString()
+    {
+        return key + "=" + value;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeAuthenticationSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeAuthenticationSelector.java
new file mode 100644
index 0000000..f1e22f2
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeAuthenticationSelector.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * An authentication selector that delegates to another selector but only if a repository has no authentication data
+ * yet. If authentication has already been assigned to a repository, that is selected.
+ */
+public final class ConservativeAuthenticationSelector
+    implements AuthenticationSelector
+{
+
+    private final AuthenticationSelector selector;
+
+    /**
+     * Creates a new selector that delegates to the specified selector.
+     *
+     * @param selector The selector to delegate to in case a repository has no authentication yet, must not be
+     *            {@code null}.
+     */
+    public ConservativeAuthenticationSelector( AuthenticationSelector selector )
+    {
+        this.selector = requireNonNull( selector, "authentication selector cannot be null" );
+    }
+
+    public Authentication getAuthentication( RemoteRepository repository )
+    {
+        Authentication auth = repository.getAuthentication();
+        if ( auth != null )
+        {
+            return auth;
+        }
+        return selector.getAuthentication( repository );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeProxySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeProxySelector.java
new file mode 100644
index 0000000..c71fe13
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ConservativeProxySelector.java
@@ -0,0 +1,58 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A proxy selector that delegates to another selector but only if a repository has no proxy yet. If a proxy has already
+ * been assigned to a repository, that is selected.
+ */
+public final class ConservativeProxySelector
+    implements ProxySelector
+{
+
+    private final ProxySelector selector;
+
+    /**
+     * Creates a new selector that delegates to the specified selector.
+     *
+     * @param selector The selector to delegate to in case a repository has no proxy yet, must not be {@code null}.
+     */
+    public ConservativeProxySelector( ProxySelector selector )
+    {
+        this.selector = requireNonNull( selector, "proxy selector cannot be null" );
+    }
+
+    public Proxy getProxy( RemoteRepository repository )
+    {
+        Proxy proxy = repository.getProxy();
+        if ( proxy != null )
+        {
+            return proxy;
+        }
+        return selector.getProxy( repository );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultAuthenticationSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultAuthenticationSelector.java
new file mode 100644
index 0000000..a5d4ce3
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultAuthenticationSelector.java
@@ -0,0 +1,64 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A simple authentication selector that selects authentication based on repository identifiers.
+ */
+public final class DefaultAuthenticationSelector
+    implements AuthenticationSelector
+{
+
+    private final Map<String, Authentication> repos = new HashMap<String, Authentication>();
+
+    /**
+     * Adds the specified authentication info for the given repository identifier.
+     * 
+     * @param id The identifier of the repository to add the authentication for, must not be {@code null}.
+     * @param auth The authentication to add, may be {@code null}.
+     * @return This selector for chaining, never {@code null}.
+     */
+    public DefaultAuthenticationSelector add( String id, Authentication auth )
+    {
+        if ( auth != null )
+        {
+            repos.put( id, auth );
+        }
+        else
+        {
+            repos.remove( id );
+        }
+
+        return this;
+    }
+
+    public Authentication getAuthentication( RemoteRepository repository )
+    {
+        return repos.get( repository.getId() );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultMirrorSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultMirrorSelector.java
new file mode 100644
index 0000000..71b3da4
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultMirrorSelector.java
@@ -0,0 +1,273 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A simple mirror selector that selects mirrors based on repository identifiers.
+ */
+public final class DefaultMirrorSelector
+    implements MirrorSelector
+{
+
+    private static final String WILDCARD = "*";
+
+    private static final String EXTERNAL_WILDCARD = "external:*";
+
+    private final List<MirrorDef> mirrors = new ArrayList<MirrorDef>();
+
+    /**
+     * Adds the specified mirror to this selector.
+     * 
+     * @param id The identifier of the mirror, must not be {@code null}.
+     * @param url The URL of the mirror, must not be {@code null}.
+     * @param type The content type of the mirror, must not be {@code null}.
+     * @param repositoryManager A flag whether the mirror is a repository manager or a simple server.
+     * @param mirrorOfIds The identifier(s) of remote repositories to mirror, must not be {@code null}. Multiple
+     *            identifiers can be separated by comma and additionally the wildcards "*" and "external:*" can be used
+     *            to match all (external) repositories, prefixing a repo id with an exclamation mark allows to express
+     *            an exclusion. For example "external:*,!central".
+     * @param mirrorOfTypes The content type(s) of remote repositories to mirror, may be {@code null} or empty to match
+     *            any content type. Similar to the repo id specification, multiple types can be comma-separated, the
+     *            wildcard "*" and the "!" negation syntax are supported. For example "*,!p2".
+     * @return This selector for chaining, never {@code null}.
+     */
+    public DefaultMirrorSelector add( String id, String url, String type, boolean repositoryManager,
+                                      String mirrorOfIds, String mirrorOfTypes )
+    {
+        mirrors.add( new MirrorDef( id, url, type, repositoryManager, mirrorOfIds, mirrorOfTypes ) );
+
+        return this;
+    }
+
+    public RemoteRepository getMirror( RemoteRepository repository )
+    {
+        MirrorDef mirror = findMirror( repository );
+
+        if ( mirror == null )
+        {
+            return null;
+        }
+
+        RemoteRepository.Builder builder =
+            new RemoteRepository.Builder( mirror.id, repository.getContentType(), mirror.url );
+
+        builder.setRepositoryManager( mirror.repositoryManager );
+
+        if ( mirror.type != null && mirror.type.length() > 0 )
+        {
+            builder.setContentType( mirror.type );
+        }
+
+        builder.setSnapshotPolicy( repository.getPolicy( true ) );
+        builder.setReleasePolicy( repository.getPolicy( false ) );
+
+        builder.setMirroredRepositories( Collections.singletonList( repository ) );
+
+        return builder.build();
+    }
+
+    private MirrorDef findMirror( RemoteRepository repository )
+    {
+        String repoId = repository.getId();
+
+        if ( repoId != null && !mirrors.isEmpty() )
+        {
+            for ( MirrorDef mirror : mirrors )
+            {
+                if ( repoId.equals( mirror.mirrorOfIds ) && matchesType( repository.getContentType(),
+                                                                         mirror.mirrorOfTypes ) )
+                {
+                    return mirror;
+                }
+            }
+
+            for ( MirrorDef mirror : mirrors )
+            {
+                if ( matchPattern( repository, mirror.mirrorOfIds ) && matchesType( repository.getContentType(),
+                                                                                    mirror.mirrorOfTypes ) )
+                {
+                    return mirror;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This method checks if the pattern matches the originalRepository. Valid patterns: * = everything external:* =
+     * everything not on the localhost and not file based. repo,repo1 = repo or repo1 *,!repo1 = everything except repo1
+     * 
+     * @param repository to compare for a match.
+     * @param pattern used for match. Currently only '*' is supported.
+     * @return true if the repository is a match to this pattern.
+     */
+    static boolean matchPattern( RemoteRepository repository, String pattern )
+    {
+        boolean result = false;
+        String originalId = repository.getId();
+
+        // simple checks first to short circuit processing below.
+        if ( WILDCARD.equals( pattern ) || pattern.equals( originalId ) )
+        {
+            result = true;
+        }
+        else
+        {
+            // process the list
+            String[] repos = pattern.split( "," );
+            for ( String repo : repos )
+            {
+                // see if this is a negative match
+                if ( repo.length() > 1 && repo.startsWith( "!" ) )
+                {
+                    if ( repo.substring( 1 ).equals( originalId ) )
+                    {
+                        // explicitly exclude. Set result and stop processing.
+                        result = false;
+                        break;
+                    }
+                }
+                // check for exact match
+                else if ( repo.equals( originalId ) )
+                {
+                    result = true;
+                    break;
+                }
+                // check for external:*
+                else if ( EXTERNAL_WILDCARD.equals( repo ) && isExternalRepo( repository ) )
+                {
+                    result = true;
+                    // don't stop processing in case a future segment explicitly excludes this repo
+                }
+                else if ( WILDCARD.equals( repo ) )
+                {
+                    result = true;
+                    // don't stop processing in case a future segment explicitly excludes this repo
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Checks the URL to see if this repository refers to an external repository.
+     * 
+     * @param repository The repository to check, must not be {@code null}.
+     * @return {@code true} if external, {@code false} otherwise.
+     */
+    static boolean isExternalRepo( RemoteRepository repository )
+    {
+        boolean local =
+            "localhost".equals( repository.getHost() ) || "127.0.0.1".equals( repository.getHost() )
+                || "file".equalsIgnoreCase( repository.getProtocol() );
+        return !local;
+    }
+
+    /**
+     * Checks whether the types configured for a mirror match with the type of the repository.
+     * 
+     * @param repoType The type of the repository, may be {@code null}.
+     * @param mirrorType The types supported by the mirror, may be {@code null}.
+     * @return {@code true} if the types associated with the mirror match the type of the original repository,
+     *         {@code false} otherwise.
+     */
+    static boolean matchesType( String repoType, String mirrorType )
+    {
+        boolean result = false;
+
+        // simple checks first to short circuit processing below.
+        if ( mirrorType == null || mirrorType.length() <= 0 || WILDCARD.equals( mirrorType ) )
+        {
+            result = true;
+        }
+        else if ( mirrorType.equals( repoType ) )
+        {
+            result = true;
+        }
+        else
+        {
+            // process the list
+            String[] layouts = mirrorType.split( "," );
+            for ( String layout : layouts )
+            {
+                // see if this is a negative match
+                if ( layout.length() > 1 && layout.startsWith( "!" ) )
+                {
+                    if ( layout.substring( 1 ).equals( repoType ) )
+                    {
+                        // explicitly exclude. Set result and stop processing.
+                        result = false;
+                        break;
+                    }
+                }
+                // check for exact match
+                else if ( layout.equals( repoType ) )
+                {
+                    result = true;
+                    break;
+                }
+                else if ( WILDCARD.equals( layout ) )
+                {
+                    result = true;
+                    // don't stop processing in case a future segment explicitly excludes this repo
+                }
+            }
+        }
+
+        return result;
+    }
+
+    static class MirrorDef
+    {
+
+        final String id;
+
+        final String url;
+
+        final String type;
+
+        final boolean repositoryManager;
+
+        final String mirrorOfIds;
+
+        final String mirrorOfTypes;
+
+        public MirrorDef( String id, String url, String type, boolean repositoryManager, String mirrorOfIds,
+                          String mirrorOfTypes )
+        {
+            this.id = id;
+            this.url = url;
+            this.type = type;
+            this.repositoryManager = repositoryManager;
+            this.mirrorOfIds = mirrorOfIds;
+            this.mirrorOfTypes = mirrorOfTypes;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultProxySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultProxySelector.java
new file mode 100644
index 0000000..429fd34
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/DefaultProxySelector.java
@@ -0,0 +1,154 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A simple proxy selector that selects the first matching proxy from a list of configured proxies.
+ */
+public final class DefaultProxySelector
+    implements ProxySelector
+{
+
+    private List<ProxyDef> proxies = new ArrayList<ProxyDef>();
+
+    /**
+     * Adds the specified proxy definition to the selector. Proxy definitions are ordered, the first matching proxy for
+     * a given repository will be used.
+     * 
+     * @param proxy The proxy definition to add, must not be {@code null}.
+     * @param nonProxyHosts The list of (case-insensitive) host names to exclude from proxying, may be {@code null}.
+     * @return This proxy selector for chaining, never {@code null}.
+     */
+    public DefaultProxySelector add( Proxy proxy, String nonProxyHosts )
+    {
+        requireNonNull( proxy, "proxy cannot be null" );
+        proxies.add( new ProxyDef( proxy, nonProxyHosts ) );
+
+        return this;
+    }
+
+    public Proxy getProxy( RemoteRepository repository )
+    {
+        Map<String, ProxyDef> candidates = new HashMap<String, ProxyDef>();
+
+        String host = repository.getHost();
+        for ( ProxyDef proxy : proxies )
+        {
+            if ( !proxy.nonProxyHosts.isNonProxyHost( host ) )
+            {
+                String key = proxy.proxy.getType().toLowerCase( Locale.ENGLISH );
+                if ( !candidates.containsKey( key ) )
+                {
+                    candidates.put( key, proxy );
+                }
+            }
+        }
+
+        String protocol = repository.getProtocol().toLowerCase( Locale.ENGLISH );
+
+        if ( "davs".equals( protocol ) )
+        {
+            protocol = "https";
+        }
+        else if ( "dav".equals( protocol ) )
+        {
+            protocol = "http";
+        }
+        else if ( protocol.startsWith( "dav:" ) )
+        {
+            protocol = protocol.substring( "dav:".length() );
+        }
+
+        ProxyDef proxy = candidates.get( protocol );
+        if ( proxy == null && "https".equals( protocol ) )
+        {
+            proxy = candidates.get( "http" );
+        }
+
+        return ( proxy != null ) ? proxy.proxy : null;
+    }
+
+    static class NonProxyHosts
+    {
+
+        private final Pattern[] patterns;
+
+        public NonProxyHosts( String nonProxyHosts )
+        {
+            List<Pattern> patterns = new ArrayList<Pattern>();
+            if ( nonProxyHosts != null )
+            {
+                for ( StringTokenizer tokenizer = new StringTokenizer( nonProxyHosts, "|" ); tokenizer.hasMoreTokens(); )
+                {
+                    String pattern = tokenizer.nextToken();
+                    pattern = pattern.replace( ".", "\\." ).replace( "*", ".*" );
+                    patterns.add( Pattern.compile( pattern, Pattern.CASE_INSENSITIVE ) );
+                }
+            }
+            this.patterns = patterns.toArray( new Pattern[patterns.size()] );
+        }
+
+        boolean isNonProxyHost( String host )
+        {
+            if ( host != null )
+            {
+                for ( Pattern pattern : patterns )
+                {
+                    if ( pattern.matcher( host ).matches() )
+                    {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+    }
+
+    static class ProxyDef
+    {
+
+        final Proxy proxy;
+
+        final NonProxyHosts nonProxyHosts;
+
+        public ProxyDef( Proxy proxy, String nonProxyHosts )
+        {
+            this.proxy = proxy;
+            this.nonProxyHosts = new NonProxyHosts( nonProxyHosts );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/JreProxySelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/JreProxySelector.java
new file mode 100644
index 0000000..a09b435
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/JreProxySelector.java
@@ -0,0 +1,182 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.net.Authenticator;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A proxy selector that uses the {@link java.net.ProxySelector#getDefault() JRE's global proxy selector}. In
+ * combination with the system property {@code java.net.useSystemProxies}, this proxy selector can be employed to pick
+ * up the proxy configuration from the operating system, see <a
+ * href="http://docs.oracle.com/javase/6/docs/technotes/guides/net/proxies.html">Java Networking and Proxies</a> for
+ * details. The {@link java.net.Authenticator JRE's global authenticator} is used to look up credentials for a proxy
+ * when needed.
+ */
+public final class JreProxySelector
+    implements ProxySelector
+{
+
+    /**
+     * Creates a new proxy selector that delegates to {@link java.net.ProxySelector#getDefault()}.
+     */
+    public JreProxySelector()
+    {
+    }
+
+    public Proxy getProxy( RemoteRepository repository )
+    {
+        List<java.net.Proxy> proxies = null;
+        try
+        {
+            URI uri = new URI( repository.getUrl() ).parseServerAuthority();
+            proxies = java.net.ProxySelector.getDefault().select( uri );
+        }
+        catch ( Exception e )
+        {
+            // URL invalid or not accepted by selector or no selector at all, simply use no proxy
+        }
+        if ( proxies != null )
+        {
+            for ( java.net.Proxy proxy : proxies )
+            {
+                if ( java.net.Proxy.Type.DIRECT.equals( proxy.type() ) )
+                {
+                    break;
+                }
+                if ( java.net.Proxy.Type.HTTP.equals( proxy.type() ) && isValid( proxy.address() ) )
+                {
+                    InetSocketAddress addr = (InetSocketAddress) proxy.address();
+                    return new Proxy( Proxy.TYPE_HTTP, addr.getHostName(), addr.getPort(),
+                                      JreProxyAuthentication.INSTANCE );
+                }
+            }
+        }
+        return null;
+    }
+
+    private static boolean isValid( SocketAddress address )
+    {
+        if ( address instanceof InetSocketAddress )
+        {
+            /*
+             * NOTE: On some platforms with java.net.useSystemProxies=true, unconfigured proxies show up as proxy
+             * objects with empty host and port 0.
+             */
+            InetSocketAddress addr = (InetSocketAddress) address;
+            if ( addr.getPort() <= 0 )
+            {
+                return false;
+            }
+            if ( addr.getHostName() == null || addr.getHostName().length() <= 0 )
+            {
+                return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private static final class JreProxyAuthentication
+        implements Authentication
+    {
+
+        public static final Authentication INSTANCE = new JreProxyAuthentication();
+
+        public void fill( AuthenticationContext context, String key, Map<String, String> data )
+        {
+            Proxy proxy = context.getProxy();
+            if ( proxy == null )
+            {
+                return;
+            }
+            if ( !AuthenticationContext.USERNAME.equals( key ) && !AuthenticationContext.PASSWORD.equals( key ) )
+            {
+                return;
+            }
+
+            try
+            {
+                URL url;
+                try
+                {
+                    url = new URL( context.getRepository().getUrl() );
+                }
+                catch ( Exception e )
+                {
+                    url = null;
+                }
+
+                PasswordAuthentication auth =
+                    Authenticator.requestPasswordAuthentication( proxy.getHost(), null, proxy.getPort(), "http",
+                                                                 "Credentials for proxy " + proxy, null, url,
+                                                                 Authenticator.RequestorType.PROXY );
+                if ( auth != null )
+                {
+                    context.put( AuthenticationContext.USERNAME, auth.getUserName() );
+                    context.put( AuthenticationContext.PASSWORD, auth.getPassword() );
+                }
+                else
+                {
+                    context.put( AuthenticationContext.USERNAME, System.getProperty( "http.proxyUser" ) );
+                    context.put( AuthenticationContext.PASSWORD, System.getProperty( "http.proxyPassword" ) );
+                }
+            }
+            catch ( SecurityException e )
+            {
+                // oh well, let's hope the proxy can do without auth
+            }
+        }
+
+        public void digest( AuthenticationDigest digest )
+        {
+            // we don't know anything about the JRE's current authenticator, assume the worst (i.e. interactive)
+            digest.update( UUID.randomUUID().toString() );
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            return this == obj || ( obj != null && getClass().equals( obj.getClass() ) );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return getClass().hashCode();
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SecretAuthentication.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SecretAuthentication.java
new file mode 100644
index 0000000..445d76c
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SecretAuthentication.java
@@ -0,0 +1,182 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+
+/**
+ * Authentication block that manages a single authentication key and its secret string value (password, passphrase).
+ * Unlike {@link StringAuthentication}, the string value is kept in an encrypted buffer and only decrypted when needed
+ * to reduce the potential of leaking the secret in a heap dump.
+ */
+final class SecretAuthentication
+    implements Authentication
+{
+
+    private static final Object[] KEYS;
+
+    static
+    {
+        KEYS = new Object[16];
+        for ( int i = 0; i < KEYS.length; i++ )
+        {
+            KEYS[i] = new Object();
+        }
+    }
+
+    private final String key;
+
+    private final char[] value;
+
+    private final int secretHash;
+
+    public SecretAuthentication( String key, String value )
+    {
+        this( ( value != null ) ? value.toCharArray() : null, key );
+    }
+
+    public SecretAuthentication( String key, char[] value )
+    {
+        this( copy( value ), key );
+    }
+
+    private SecretAuthentication( char[] value, String key )
+    {
+        this.key = requireNonNull( key, "authentication key cannot be null" );
+        if ( key.length() == 0 )
+        {
+            throw new IllegalArgumentException( "authentication key cannot be empty" );
+        }
+        this.secretHash = Arrays.hashCode( value ) ^ KEYS[0].hashCode();
+        this.value = xor( value );
+    }
+
+    private static char[] copy( char[] chars )
+    {
+        return ( chars != null ) ? chars.clone() : null;
+    }
+
+    private char[] xor( char[] chars )
+    {
+        if ( chars != null )
+        {
+            int mask = System.identityHashCode( this );
+            for ( int i = 0; i < chars.length; i++ )
+            {
+                int key = KEYS[( i >> 1 ) % KEYS.length].hashCode();
+                key ^= mask;
+                chars[i] ^= ( ( i & 1 ) == 0 ) ? ( key & 0xFFFF ) : ( key >>> 16 );
+            }
+        }
+        return chars;
+    }
+
+    private static void clear( char[] chars )
+    {
+        if ( chars != null )
+        {
+            for ( int i = 0; i < chars.length; i++ )
+            {
+                chars[i] = '\0';
+            }
+        }
+    }
+
+    public void fill( AuthenticationContext context, String key, Map<String, String> data )
+    {
+        char[] secret = copy( value );
+        xor( secret );
+        context.put( this.key, secret );
+        // secret will be cleared upon AuthenticationContext.close()
+    }
+
+    public void digest( AuthenticationDigest digest )
+    {
+        char[] secret = copy( value );
+        try
+        {
+            xor( secret );
+            digest.update( key );
+            digest.update( secret );
+        }
+        finally
+        {
+            clear( secret );
+        }
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        SecretAuthentication that = (SecretAuthentication) obj;
+        if ( !eq( key, that.key ) || secretHash != that.secretHash )
+        {
+            return false;
+        }
+        char[] secret = copy( value );
+        char[] thatSecret = copy( that.value );
+        try
+        {
+            xor( secret );
+            that.xor( thatSecret );
+            return Arrays.equals( secret, thatSecret );
+        }
+        finally
+        {
+            clear( secret );
+            clear( thatSecret );
+        }
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + key.hashCode();
+        hash = hash * 31 + secretHash;
+        return hash;
+    }
+
+    @Override
+    public String toString()
+    {
+        return key + "=" + ( ( value != null ) ? "***" : "null" );
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleArtifactDescriptorPolicy.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleArtifactDescriptorPolicy.java
new file mode 100644
index 0000000..ccf1ba8
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleArtifactDescriptorPolicy.java
@@ -0,0 +1,61 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
+import org.eclipse.aether.resolution.ArtifactDescriptorPolicyRequest;
+
+/**
+ * An artifact descriptor error policy that allows to control error handling at a global level.
+ */
+public final class SimpleArtifactDescriptorPolicy
+    implements ArtifactDescriptorPolicy
+{
+
+    private final int policy;
+
+    /**
+     * Creates a new error policy with the specified behavior.
+     * 
+     * @param ignoreMissing {@code true} to ignore missing descriptors, {@code false} to fail resolution.
+     * @param ignoreInvalid {@code true} to ignore invalid descriptors, {@code false} to fail resolution.
+     */
+    public SimpleArtifactDescriptorPolicy( boolean ignoreMissing, boolean ignoreInvalid )
+    {
+        this( ( ignoreMissing ? IGNORE_MISSING : 0 ) | ( ignoreInvalid ? IGNORE_INVALID : 0 ) );
+    }
+
+    /**
+     * Creates a new error policy with the specified bit mask.
+     * 
+     * @param policy The bit mask describing the policy.
+     */
+    public SimpleArtifactDescriptorPolicy( int policy )
+    {
+        this.policy = policy;
+    }
+
+    public int getPolicy( RepositorySystemSession session, ArtifactDescriptorPolicyRequest request )
+    {
+        return policy;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleResolutionErrorPolicy.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleResolutionErrorPolicy.java
new file mode 100644
index 0000000..4fa9059
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/SimpleResolutionErrorPolicy.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest;
+
+/**
+ * A resolution error policy that allows to control caching for artifacts and metadata at a global level.
+ */
+public final class SimpleResolutionErrorPolicy
+    implements ResolutionErrorPolicy
+{
+
+    private final int artifactPolicy;
+
+    private final int metadataPolicy;
+
+    /**
+     * Creates a new error policy with the specified behavior for both artifacts and metadata.
+     * 
+     * @param cacheNotFound {@code true} to enable caching of missing items, {@code false} to disable it.
+     * @param cacheTransferErrors {@code true} to enable chaching of transfer errors, {@code false} to disable it.
+     */
+    public SimpleResolutionErrorPolicy( boolean cacheNotFound, boolean cacheTransferErrors )
+    {
+        this( ( cacheNotFound ? CACHE_NOT_FOUND : 0 ) | ( cacheTransferErrors ? CACHE_TRANSFER_ERROR : 0 ) );
+    }
+
+    /**
+     * Creates a new error policy with the specified bit mask for both artifacts and metadata.
+     * 
+     * @param policy The bit mask describing the policy for artifacts and metadata.
+     */
+    public SimpleResolutionErrorPolicy( int policy )
+    {
+        this( policy, policy );
+    }
+
+    /**
+     * Creates a new error policy with the specified bit masks for artifacts and metadata.
+     * 
+     * @param artifactPolicy The bit mask describing the policy for artifacts.
+     * @param metadataPolicy The bit mask describing the policy for metadata.
+     */
+    public SimpleResolutionErrorPolicy( int artifactPolicy, int metadataPolicy )
+    {
+        this.artifactPolicy = artifactPolicy;
+        this.metadataPolicy = metadataPolicy;
+    }
+
+    public int getArtifactPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Artifact> request )
+    {
+        return artifactPolicy;
+    }
+
+    public int getMetadataPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest<Metadata> request )
+    {
+        return metadataPolicy;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/StringAuthentication.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/StringAuthentication.java
new file mode 100644
index 0000000..606d8f2
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/StringAuthentication.java
@@ -0,0 +1,95 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import java.util.Map;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+
+/**
+ * Authentication block that manages a single authentication key and its string value.
+ */
+final class StringAuthentication
+    implements Authentication
+{
+
+    private final String key;
+
+    private final String value;
+
+    public StringAuthentication( String key, String value )
+    {
+        this.key = requireNonNull( key, "authentication key cannot be null" );
+        if ( key.length() == 0 )
+        {
+            throw new IllegalArgumentException( "authentication key cannot be empty" );
+        }
+        this.value = value;
+    }
+
+    public void fill( AuthenticationContext context, String key, Map<String, String> data )
+    {
+        context.put( this.key, value );
+    }
+
+    public void digest( AuthenticationDigest digest )
+    {
+        digest.update( key, value );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+        StringAuthentication that = (StringAuthentication) obj;
+        return eq( key, that.key ) && eq( value, that.value );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + key.hashCode();
+        hash = hash * 31 + ( ( value != null ) ? value.hashCode() : 0 );
+        return hash;
+    }
+
+    @Override
+    public String toString()
+    {
+        return key + "=" + value;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/package-info.java
new file mode 100644
index 0000000..1c0a194
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Ready-to-use selectors for authentication, proxies and mirrors and a few other repository related utilities.
+ */
+package org.eclipse.aether.util.repository;
+
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
new file mode 100644
index 0000000..3596a29
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java
@@ -0,0 +1,464 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.aether.version.Version;
+
+/**
+ * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See
+ * {@link GenericVersionScheme} for details.
+ */
+final class GenericVersion
+    implements Version
+{
+
+    private final String version;
+
+    private final Item[] items;
+
+    private final int hash;
+
+    /**
+     * Creates a generic version from the specified string.
+     * 
+     * @param version The version string, must not be {@code null}.
+     */
+    public GenericVersion( String version )
+    {
+        this.version = version;
+        items = parse( version );
+        hash = Arrays.hashCode( items );
+    }
+
+    private static Item[] parse( String version )
+    {
+        List<Item> items = new ArrayList<Item>();
+
+        for ( Tokenizer tokenizer = new Tokenizer( version ); tokenizer.next(); )
+        {
+            Item item = tokenizer.toItem();
+            items.add( item );
+        }
+
+        trimPadding( items );
+
+        return items.toArray( new Item[items.size()] );
+    }
+
+    private static void trimPadding( List<Item> items )
+    {
+        Boolean number = null;
+        int end = items.size() - 1;
+        for ( int i = end; i > 0; i-- )
+        {
+            Item item = items.get( i );
+            if ( !Boolean.valueOf( item.isNumber() ).equals( number ) )
+            {
+                end = i;
+                number = item.isNumber();
+            }
+            if ( end == i && ( i == items.size() - 1 || items.get( i - 1 ).isNumber() == item.isNumber() )
+                && item.compareTo( null ) == 0 )
+            {
+                items.remove( i );
+                end--;
+            }
+        }
+    }
+
+    public int compareTo( Version obj )
+    {
+        final Item[] these = items;
+        final Item[] those = ( (GenericVersion) obj ).items;
+
+        boolean number = true;
+
+        for ( int index = 0;; index++ )
+        {
+            if ( index >= these.length && index >= those.length )
+            {
+                return 0;
+            }
+            else if ( index >= these.length )
+            {
+                return -comparePadding( those, index, null );
+            }
+            else if ( index >= those.length )
+            {
+                return comparePadding( these, index, null );
+            }
+
+            Item thisItem = these[index];
+            Item thatItem = those[index];
+
+            if ( thisItem.isNumber() != thatItem.isNumber() )
+            {
+                if ( number == thisItem.isNumber() )
+                {
+                    return comparePadding( these, index, number );
+                }
+                else
+                {
+                    return -comparePadding( those, index, number );
+                }
+            }
+            else
+            {
+                int rel = thisItem.compareTo( thatItem );
+                if ( rel != 0 )
+                {
+                    return rel;
+                }
+                number = thisItem.isNumber();
+            }
+        }
+    }
+
+    private static int comparePadding( Item[] items, int index, Boolean number )
+    {
+        int rel = 0;
+        for ( int i = index; i < items.length; i++ )
+        {
+            Item item = items[i];
+            if ( number != null && number != item.isNumber() )
+            {
+                break;
+            }
+            rel = item.compareTo( null );
+            if ( rel != 0 )
+            {
+                break;
+            }
+        }
+        return rel;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        return ( obj instanceof GenericVersion ) && compareTo( (GenericVersion) obj ) == 0;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return hash;
+    }
+
+    @Override
+    public String toString()
+    {
+        return version;
+    }
+
+    static final class Tokenizer
+    {
+
+        private static final Integer QUALIFIER_ALPHA = -5;
+
+        private static final Integer QUALIFIER_BETA = -4;
+
+        private static final Integer QUALIFIER_MILESTONE = -3;
+
+        private static final Map<String, Integer> QUALIFIERS;
+
+        static
+        {
+            QUALIFIERS = new TreeMap<String, Integer>( String.CASE_INSENSITIVE_ORDER );
+            QUALIFIERS.put( "alpha", QUALIFIER_ALPHA );
+            QUALIFIERS.put( "beta", QUALIFIER_BETA );
+            QUALIFIERS.put( "milestone", QUALIFIER_MILESTONE );
+            QUALIFIERS.put( "cr", -2 );
+            QUALIFIERS.put( "rc", -2 );
+            QUALIFIERS.put( "snapshot", -1 );
+            QUALIFIERS.put( "ga", 0 );
+            QUALIFIERS.put( "final", 0 );
+            QUALIFIERS.put( "", 0 );
+            QUALIFIERS.put( "sp", 1 );
+        }
+
+        private final String version;
+
+        private int index;
+
+        private String token;
+
+        private boolean number;
+
+        private boolean terminatedByNumber;
+
+        public Tokenizer( String version )
+        {
+            this.version = ( version.length() > 0 ) ? version : "0";
+        }
+
+        public boolean next()
+        {
+            final int n = version.length();
+            if ( index >= n )
+            {
+                return false;
+            }
+
+            int state = -2;
+
+            int start = index;
+            int end = n;
+            terminatedByNumber = false;
+
+            for ( ; index < n; index++ )
+            {
+                char c = version.charAt( index );
+
+                if ( c == '.' || c == '-' || c == '_' )
+                {
+                    end = index;
+                    index++;
+                    break;
+                }
+                else
+                {
+                    int digit = Character.digit( c, 10 );
+                    if ( digit >= 0 )
+                    {
+                        if ( state == -1 )
+                        {
+                            end = index;
+                            terminatedByNumber = true;
+                            break;
+                        }
+                        if ( state == 0 )
+                        {
+                            // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
+                            start++;
+                        }
+                        state = ( state > 0 || digit > 0 ) ? 1 : 0;
+                    }
+                    else
+                    {
+                        if ( state >= 0 )
+                        {
+                            end = index;
+                            break;
+                        }
+                        state = -1;
+                    }
+                }
+
+            }
+
+            if ( end - start > 0 )
+            {
+                token = version.substring( start, end );
+                number = state >= 0;
+            }
+            else
+            {
+                token = "0";
+                number = true;
+            }
+
+            return true;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( token );
+        }
+
+        public Item toItem()
+        {
+            if ( number )
+            {
+                try
+                {
+                    if ( token.length() < 10 )
+                    {
+                        return new Item( Item.KIND_INT, Integer.parseInt( token ) );
+                    }
+                    else
+                    {
+                        return new Item( Item.KIND_BIGINT, new BigInteger( token ) );
+                    }
+                }
+                catch ( NumberFormatException e )
+                {
+                    throw new IllegalStateException( e );
+                }
+            }
+            else
+            {
+                if ( index >= version.length() )
+                {
+                    if ( "min".equalsIgnoreCase( token ) )
+                    {
+                        return Item.MIN;
+                    }
+                    else if ( "max".equalsIgnoreCase( token ) )
+                    {
+                        return Item.MAX;
+                    }
+                }
+                if ( terminatedByNumber && token.length() == 1 )
+                {
+                    switch ( token.charAt( 0 ) )
+                    {
+                        case 'a':
+                        case 'A':
+                            return new Item( Item.KIND_QUALIFIER, QUALIFIER_ALPHA );
+                        case 'b':
+                        case 'B':
+                            return new Item( Item.KIND_QUALIFIER, QUALIFIER_BETA );
+                        case 'm':
+                        case 'M':
+                            return new Item( Item.KIND_QUALIFIER, QUALIFIER_MILESTONE );
+                        default:
+                    }
+                }
+                Integer qualifier = QUALIFIERS.get( token );
+                if ( qualifier != null )
+                {
+                    return new Item( Item.KIND_QUALIFIER, qualifier );
+                }
+                else
+                {
+                    return new Item( Item.KIND_STRING, token.toLowerCase( Locale.ENGLISH ) );
+                }
+            }
+        }
+
+    }
+
+    static final class Item
+    {
+
+        static final int KIND_MAX = 8;
+
+        static final int KIND_BIGINT = 5;
+
+        static final int KIND_INT = 4;
+
+        static final int KIND_STRING = 3;
+
+        static final int KIND_QUALIFIER = 2;
+
+        static final int KIND_MIN = 0;
+
+        static final Item MAX = new Item( KIND_MAX, "max" );
+
+        static final Item MIN = new Item( KIND_MIN, "min" );
+
+        private final int kind;
+
+        private final Object value;
+
+        public Item( int kind, Object value )
+        {
+            this.kind = kind;
+            this.value = value;
+        }
+
+        public boolean isNumber()
+        {
+            return ( kind & KIND_QUALIFIER ) == 0; // i.e. kind != string/qualifier
+        }
+
+        public int compareTo( Item that )
+        {
+            int rel;
+            if ( that == null )
+            {
+                // null in this context denotes the pad item (0 or "ga")
+                switch ( kind )
+                {
+                    case KIND_MIN:
+                        rel = -1;
+                        break;
+                    case KIND_MAX:
+                    case KIND_BIGINT:
+                    case KIND_STRING:
+                        rel = 1;
+                        break;
+                    case KIND_INT:
+                    case KIND_QUALIFIER:
+                        rel = (Integer) value;
+                        break;
+                    default:
+                        throw new IllegalStateException( "unknown version item kind " + kind );
+                }
+            }
+            else
+            {
+                rel = kind - that.kind;
+                if ( rel == 0 )
+                {
+                    switch ( kind )
+                    {
+                        case KIND_MAX:
+                        case KIND_MIN:
+                            break;
+                        case KIND_BIGINT:
+                            rel = ( (BigInteger) value ).compareTo( (BigInteger) that.value );
+                            break;
+                        case KIND_INT:
+                        case KIND_QUALIFIER:
+                            rel = ( (Integer) value ).compareTo( (Integer) that.value );
+                            break;
+                        case KIND_STRING:
+                            rel = ( (String) value ).compareToIgnoreCase( (String) that.value );
+                            break;
+                        default:
+                            throw new IllegalStateException( "unknown version item kind " + kind );
+                    }
+                }
+            }
+            return rel;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            return ( obj instanceof Item ) && compareTo( (Item) obj ) == 0;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return value.hashCode() + kind * 31;
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( value );
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionConstraint.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionConstraint.java
new file mode 100644
index 0000000..27c52fa
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionConstraint.java
@@ -0,0 +1,125 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+
+/**
+ * A constraint on versions for a dependency.
+ */
+final class GenericVersionConstraint
+    implements VersionConstraint
+{
+
+    private final VersionRange range;
+
+    private final Version version;
+
+    /**
+     * Creates a version constraint from the specified version range.
+     *
+     * @param range The version range, must not be {@code null}.
+     */
+    public GenericVersionConstraint( VersionRange range )
+    {
+        this.range = requireNonNull( range, "version range cannot be null" );
+        this.version = null;
+    }
+
+    /**
+     * Creates a version constraint from the specified version.
+     *
+     * @param version The version, must not be {@code null}.
+     */
+    public GenericVersionConstraint( Version version )
+    {
+        this.version = requireNonNull( version, "version cannot be null" );
+        this.range = null;
+    }
+
+    public VersionRange getRange()
+    {
+        return range;
+    }
+
+    public Version getVersion()
+    {
+        return version;
+    }
+
+    public boolean containsVersion( Version version )
+    {
+        if ( range == null )
+        {
+            return version.equals( this.version );
+        }
+        else
+        {
+            return range.containsVersion( version );
+        }
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.valueOf( ( range == null ) ? version : range );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        GenericVersionConstraint that = (GenericVersionConstraint) obj;
+
+        return eq( range, that.range ) && eq( version, that.getVersion() );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( getRange() );
+        hash = hash * 31 + hash( getVersion() );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionRange.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionRange.java
new file mode 100644
index 0000000..832dd94
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionRange.java
@@ -0,0 +1,242 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionRange;
+
+/**
+ * A version range inspired by mathematical range syntax. For example, "[1.0,2.0)", "[1.0,)" or "[1.0]".
+ */
+final class GenericVersionRange
+    implements VersionRange
+{
+
+    private final Bound lowerBound;
+
+    private final Bound upperBound;
+
+    /**
+     * Creates a version range from the specified range specification.
+     * 
+     * @param range The range specification to parse, must not be {@code null}.
+     * @throws InvalidVersionSpecificationException If the range could not be parsed.
+     */
+    public GenericVersionRange( String range )
+        throws InvalidVersionSpecificationException
+    {
+        String process = range;
+
+        boolean lowerBoundInclusive, upperBoundInclusive;
+        Version lowerBound, upperBound;
+
+        if ( range.startsWith( "[" ) )
+        {
+            lowerBoundInclusive = true;
+        }
+        else if ( range.startsWith( "(" ) )
+        {
+            lowerBoundInclusive = false;
+        }
+        else
+        {
+            throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                + ", a range must start with either [ or (" );
+        }
+
+        if ( range.endsWith( "]" ) )
+        {
+            upperBoundInclusive = true;
+        }
+        else if ( range.endsWith( ")" ) )
+        {
+            upperBoundInclusive = false;
+        }
+        else
+        {
+            throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                + ", a range must end with either [ or (" );
+        }
+
+        process = process.substring( 1, process.length() - 1 );
+
+        int index = process.indexOf( "," );
+
+        if ( index < 0 )
+        {
+            if ( !lowerBoundInclusive || !upperBoundInclusive )
+            {
+                throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                    + ", single version must be surrounded by []" );
+            }
+
+            String version = process.trim();
+            if ( version.endsWith( ".*" ) )
+            {
+                String prefix = version.substring( 0, version.length() - 1 );
+                lowerBound = parse( prefix + "min" );
+                upperBound = parse( prefix + "max" );
+            }
+            else
+            {
+                lowerBound = upperBound = parse( version );
+            }
+        }
+        else
+        {
+            String parsedLowerBound = process.substring( 0, index ).trim();
+            String parsedUpperBound = process.substring( index + 1 ).trim();
+
+            // more than two bounds, e.g. (1,2,3)
+            if ( parsedUpperBound.contains( "," ) )
+            {
+                throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                    + ", bounds may not contain additional ','" );
+            }
+
+            lowerBound = parsedLowerBound.length() > 0 ? parse( parsedLowerBound ) : null;
+            upperBound = parsedUpperBound.length() > 0 ? parse( parsedUpperBound ) : null;
+
+            if ( upperBound != null && lowerBound != null )
+            {
+                if ( upperBound.compareTo( lowerBound ) < 0 )
+                {
+                    throw new InvalidVersionSpecificationException( range, "Invalid version range " + range
+                        + ", lower bound must not be greater than upper bound" );
+                }
+            }
+        }
+
+        this.lowerBound = ( lowerBound != null ) ? new Bound( lowerBound, lowerBoundInclusive ) : null;
+        this.upperBound = ( upperBound != null ) ? new Bound( upperBound, upperBoundInclusive ) : null;
+    }
+
+    private Version parse( String version )
+    {
+        return new GenericVersion( version );
+    }
+
+    public Bound getLowerBound()
+    {
+        return lowerBound;
+    }
+
+    public Bound getUpperBound()
+    {
+        return upperBound;
+    }
+
+    public boolean containsVersion( Version version )
+    {
+        if ( lowerBound != null )
+        {
+            int comparison = lowerBound.getVersion().compareTo( version );
+
+            if ( comparison == 0 && !lowerBound.isInclusive() )
+            {
+                return false;
+            }
+            if ( comparison > 0 )
+            {
+                return false;
+            }
+        }
+
+        if ( upperBound != null )
+        {
+            int comparison = upperBound.getVersion().compareTo( version );
+
+            if ( comparison == 0 && !upperBound.isInclusive() )
+            {
+                return false;
+            }
+            if ( comparison < 0 )
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        GenericVersionRange that = (GenericVersionRange) obj;
+
+        return eq( upperBound, that.upperBound ) && eq( lowerBound, that.lowerBound );
+    }
+
+    private static <T> boolean eq( T s1, T s2 )
+    {
+        return s1 != null ? s1.equals( s2 ) : s2 == null;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 17;
+        hash = hash * 31 + hash( upperBound );
+        hash = hash * 31 + hash( lowerBound );
+        return hash;
+    }
+
+    private static int hash( Object obj )
+    {
+        return obj != null ? obj.hashCode() : 0;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 64 );
+        if ( lowerBound != null )
+        {
+            buffer.append( lowerBound.isInclusive() ? '[' : '(' );
+            buffer.append( lowerBound.getVersion() );
+        }
+        else
+        {
+            buffer.append( '(' );
+        }
+        buffer.append( ',' );
+        if ( upperBound != null )
+        {
+            buffer.append( upperBound.getVersion() );
+            buffer.append( upperBound.isInclusive() ? ']' : ')' );
+        }
+        else
+        {
+            buffer.append( ')' );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
new file mode 100644
index 0000000..8fc0488
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
@@ -0,0 +1,149 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+import org.eclipse.aether.version.VersionScheme;
+
+/**
+ * A version scheme using a generic version syntax and common sense sorting.
+ * <p>
+ * This scheme accepts versions of any form, interpreting a version as a sequence of numeric and alphabetic segments.
+ * The characters '-', '_', and '.' as well as the mere transitions from digit to letter and vice versa delimit the
+ * version segments. Delimiters are treated as equivalent.
+ * </p>
+ * <p>
+ * Numeric segments are compared mathematically, alphabetic segments are compared lexicographically and
+ * case-insensitively. However, the following qualifier strings are recognized and treated specially: "alpha" = "a" &lt;
+ * "beta" = "b" &lt; "milestone" = "m" &lt; "cr" = "rc" &lt; "snapshot" &lt; "final" = "ga" &lt; "sp". All of those
+ * well-known qualifiers are considered smaller/older than other strings. An empty segment/string is equivalent to 0.
+ * </p>
+ * <p>
+ * In addition to the above mentioned qualifiers, the tokens "min" and "max" may be used as final version segment to
+ * denote the smallest/greatest version having a given prefix. For example, "1.2.min" denotes the smallest version in
+ * the 1.2 line, "1.2.max" denotes the greatest version in the 1.2 line. A version range of the form "[M.N.*]" is short
+ * for "[M.N.min, M.N.max]".
+ * </p>
+ * <p>
+ * Numbers and strings are considered incomparable against each other. Where version segments of different kind would
+ * collide, comparison will instead assume that the previous segments are padded with trailing 0 or "ga" segments,
+ * respectively, until the kind mismatch is resolved, e.g. "1-alpha" = "1.0.0-alpha" &lt; "1.0.1-ga" = "1.0.1".
+ * </p>
+ */
+public final class GenericVersionScheme
+    implements VersionScheme
+{
+
+    /**
+     * Creates a new instance of the version scheme for parsing versions.
+     */
+    public GenericVersionScheme()
+    {
+    }
+
+    public Version parseVersion( final String version )
+        throws InvalidVersionSpecificationException
+    {
+        return new GenericVersion( version );
+    }
+
+    public VersionRange parseVersionRange( final String range )
+        throws InvalidVersionSpecificationException
+    {
+        return new GenericVersionRange( range );
+    }
+
+    public VersionConstraint parseVersionConstraint( final String constraint )
+        throws InvalidVersionSpecificationException
+    {
+        Collection<VersionRange> ranges = new ArrayList<VersionRange>();
+
+        String process = constraint;
+
+        while ( process.startsWith( "[" ) || process.startsWith( "(" ) )
+        {
+            int index1 = process.indexOf( ')' );
+            int index2 = process.indexOf( ']' );
+
+            int index = index2;
+            if ( index2 < 0 || ( index1 >= 0 && index1 < index2 ) )
+            {
+                index = index1;
+            }
+
+            if ( index < 0 )
+            {
+                throw new InvalidVersionSpecificationException( constraint, "Unbounded version range " + constraint );
+            }
+
+            VersionRange range = parseVersionRange( process.substring( 0, index + 1 ) );
+            ranges.add( range );
+
+            process = process.substring( index + 1 ).trim();
+
+            if ( process.length() > 0 && process.startsWith( "," ) )
+            {
+                process = process.substring( 1 ).trim();
+            }
+        }
+
+        if ( process.length() > 0 && !ranges.isEmpty() )
+        {
+            throw new InvalidVersionSpecificationException( constraint, "Invalid version range " + constraint
+                + ", expected [ or ( but got " + process );
+        }
+
+        VersionConstraint result;
+        if ( ranges.isEmpty() )
+        {
+            result = new GenericVersionConstraint( parseVersion( constraint ) );
+        }
+        else
+        {
+            result = new GenericVersionConstraint( UnionVersionRange.from( ranges ) );
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean equals( final Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+
+        return obj != null && getClass().equals( obj.getClass() );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return getClass().hashCode();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/UnionVersionRange.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/UnionVersionRange.java
new file mode 100644
index 0000000..c54a4b4
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/UnionVersionRange.java
@@ -0,0 +1,181 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionRange;
+
+/**
+ * A union of version ranges.
+ */
+final class UnionVersionRange
+    implements VersionRange
+{
+
+    private final Set<VersionRange> ranges;
+
+    private final Bound lowerBound;
+
+    private final Bound upperBound;
+
+    public static VersionRange from( VersionRange... ranges )
+    {
+        if ( ranges == null )
+        {
+            return from( Collections.<VersionRange>emptySet() );
+        }
+        return from( Arrays.asList( ranges ) );
+    }
+
+    public static VersionRange from( Collection<? extends VersionRange> ranges )
+    {
+        if ( ranges != null && ranges.size() == 1 )
+        {
+            return ranges.iterator().next();
+        }
+        return new UnionVersionRange( ranges );
+    }
+
+    private UnionVersionRange( Collection<? extends VersionRange> ranges )
+    {
+        if ( ranges == null || ranges.isEmpty() )
+        {
+            this.ranges = Collections.emptySet();
+            lowerBound = upperBound = null;
+        }
+        else
+        {
+            this.ranges = new HashSet<VersionRange>( ranges );
+            Bound lowerBound = null, upperBound = null;
+            for ( VersionRange range : this.ranges )
+            {
+                Bound lb = range.getLowerBound();
+                if ( lb == null )
+                {
+                    lowerBound = null;
+                    break;
+                }
+                else if ( lowerBound == null )
+                {
+                    lowerBound = lb;
+                }
+                else
+                {
+                    int c = lb.getVersion().compareTo( lowerBound.getVersion() );
+                    if ( c < 0 || ( c == 0 && !lowerBound.isInclusive() ) )
+                    {
+                        lowerBound = lb;
+                    }
+                }
+            }
+            for ( VersionRange range : this.ranges )
+            {
+                Bound ub = range.getUpperBound();
+                if ( ub == null )
+                {
+                    upperBound = null;
+                    break;
+                }
+                else if ( upperBound == null )
+                {
+                    upperBound = ub;
+                }
+                else
+                {
+                    int c = ub.getVersion().compareTo( upperBound.getVersion() );
+                    if ( c > 0 || ( c == 0 && !upperBound.isInclusive() ) )
+                    {
+                        upperBound = ub;
+                    }
+                }
+            }
+            this.lowerBound = lowerBound;
+            this.upperBound = upperBound;
+        }
+    }
+
+    public boolean containsVersion( Version version )
+    {
+        for ( VersionRange range : ranges )
+        {
+            if ( range.containsVersion( version ) )
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public Bound getLowerBound()
+    {
+        return lowerBound;
+    }
+
+    public Bound getUpperBound()
+    {
+        return upperBound;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( obj == this )
+        {
+            return true;
+        }
+        else if ( obj == null || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        UnionVersionRange that = (UnionVersionRange) obj;
+
+        return ranges.equals( that.ranges );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int hash = 97 * ranges.hashCode();
+        return hash;
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+        for ( VersionRange range : ranges )
+        {
+            if ( buffer.length() > 0 )
+            {
+                buffer.append( ", " );
+            }
+            buffer.append( range );
+        }
+        return buffer.toString();
+    }
+
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/package-info.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/package-info.java
new file mode 100644
index 0000000..18dc724
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/package-info.java
@@ -0,0 +1,24 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * Ready-to-use version schemes for parsing/comparing versions.
+ */
+package org.eclipse.aether.util.version;
+
diff --git a/maven-resolver-util/src/site/site.xml b/maven-resolver-util/src/site/site.xml
new file mode 100644
index 0000000..096b05c
--- /dev/null
+++ b/maven-resolver-util/src/site/site.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Utilities">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+    </menu>
+
+    <menu ref="parent"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java
new file mode 100644
index 0000000..b249e82
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ChecksumUtilTest.java
@@ -0,0 +1,186 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+import static org.eclipse.aether.internal.test.util.TestFileUtils.*;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.aether.util.ChecksumUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ChecksumUtilTest
+{
+    private File emptyFile;
+
+    private File patternFile;
+
+    private File textFile;
+
+    private static Map<String, String> emptyFileChecksums = new HashMap<String, String>();
+
+    private static Map<String, String> patternFileChecksums = new HashMap<String, String>();
+
+    private static Map<String, String> textFileChecksums = new HashMap<String, String>();
+
+    private Map<File, Map<String, String>> sums = new HashMap<File, Map<String, String>>();
+
+    @BeforeClass
+    public static void beforeClass()
+        throws IOException
+    {
+        emptyFileChecksums.put( "MD5", "d41d8cd98f00b204e9800998ecf8427e" );
+        emptyFileChecksums.put( "SHA-1", "da39a3ee5e6b4b0d3255bfef95601890afd80709" );
+        patternFileChecksums.put( "MD5", "14f01d6c7de7d4cf0a4887baa3528b5a" );
+        patternFileChecksums.put( "SHA-1", "feeeda19f626f9b0ef6cbf5948c1ec9531694295" );
+        textFileChecksums.put( "MD5", "12582d1a662cefe3385f2113998e43ed" );
+        textFileChecksums.put( "SHA-1", "a8ae272db549850eef2ff54376f8cac2770745ee" );
+    }
+
+    @Before
+    public void before()
+        throws IOException
+    {
+        sums.clear();
+
+        emptyFile = createTempFile( new byte[] {}, 0 );
+        sums.put( emptyFile, emptyFileChecksums );
+
+        patternFile =
+            createTempFile( new byte[] { 0, 1, 2, 4, 8, 16, 32, 64, 127, -1, -2, -4, -8, -16, -32, -64, -127 }, 1000 );
+        sums.put( patternFile, patternFileChecksums );
+
+        textFile = createTempFile( "the quick brown fox jumps over the lazy dog\n".getBytes( StandardCharsets.UTF_8 ), 500 );
+        sums.put( textFile, textFileChecksums );
+
+    }
+
+    @Test
+    public void testEquality()
+        throws Throwable
+    {
+        Map<String, Object> checksums = null;
+
+        for ( File file : new File[] { emptyFile, patternFile, textFile } )
+        {
+
+            checksums = ChecksumUtils.calc( file, Arrays.asList( "SHA-1", "MD5" ) );
+
+            for ( Entry<String, Object> entry : checksums.entrySet() )
+            {
+                if ( entry.getValue() instanceof Throwable )
+                {
+                    throw (Throwable) entry.getValue();
+                }
+                String actual = entry.getValue().toString();
+                String expected = sums.get( file ).get( entry.getKey() );
+                assertEquals( String.format( "checksums do not match for '%s', algorithm '%s'", file.getName(),
+                                             entry.getKey() ), expected, actual );
+            }
+            assertTrue( "Could not delete file", file.delete() );
+        }
+    }
+
+    @Test
+    public void testFileHandleLeakage()
+        throws IOException
+    {
+        for ( File file : new File[] { emptyFile, patternFile, textFile } )
+        {
+            for ( int i = 0; i < 150; i++ )
+            {
+                ChecksumUtils.calc( file, Arrays.asList( "SHA-1", "MD5" ) );
+            }
+            assertTrue( "Could not delete file", file.delete() );
+        }
+
+    }
+
+    @Test
+    public void testRead()
+        throws IOException
+    {
+        for ( Map<String, String> checksums : sums.values() )
+        {
+            String sha1 = checksums.get( "SHA-1" );
+            String md5 = checksums.get( "MD5" );
+
+            File sha1File = createTempFile( sha1 );
+            File md5File = createTempFile( md5 );
+
+            assertEquals( sha1, ChecksumUtils.read( sha1File ) );
+            assertEquals( md5, ChecksumUtils.read( md5File ) );
+
+            assertTrue( "ChecksumUtils leaks file handles (cannot delete checksums.sha1)", sha1File.delete() );
+            assertTrue( "ChecksumUtils leaks file handles (cannot delete checksums.md5)", md5File.delete() );
+        }
+    }
+
+    @Test
+    public void testReadSpaces()
+        throws IOException
+    {
+        for ( Map<String, String> checksums : sums.values() )
+        {
+            String sha1 = checksums.get( "SHA-1" );
+            String md5 = checksums.get( "MD5" );
+
+            File sha1File = createTempFile( "sha1-checksum = " + sha1 );
+            File md5File = createTempFile( md5 + " test" );
+
+            assertEquals( sha1, ChecksumUtils.read( sha1File ) );
+            assertEquals( md5, ChecksumUtils.read( md5File ) );
+
+            assertTrue( "ChecksumUtils leaks file handles (cannot delete checksums.sha1)", sha1File.delete() );
+            assertTrue( "ChecksumUtils leaks file handles (cannot delete checksums.md5)", md5File.delete() );
+        }
+    }
+
+    @Test
+    public void testReadEmptyFile()
+        throws IOException
+    {
+        File file = createTempFile( "" );
+
+        assertEquals( "", ChecksumUtils.read( file ) );
+
+        assertTrue( "ChecksumUtils leaks file handles (cannot delete checksum.empty)", file.delete() );
+    }
+
+    @Test
+    public void testToHexString()
+    {
+        assertEquals( null, ChecksumUtils.toHexString( null ) );
+        assertEquals( "", ChecksumUtils.toHexString( new byte[] {} ) );
+        assertEquals( "00", ChecksumUtils.toHexString( new byte[] { 0 } ) );
+        assertEquals( "ff", ChecksumUtils.toHexString( new byte[] { -1 } ) );
+        assertEquals( "00017f", ChecksumUtils.toHexString( new byte[] { 0, 1, 127 } ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
new file mode 100644
index 0000000..683c8e0
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/ConfigUtilsTest.java
@@ -0,0 +1,229 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class ConfigUtilsTest
+{
+
+    Map<Object, Object> config = new HashMap<Object, Object>();
+
+    @Test
+    public void testGetObject_Default()
+    {
+        Object val = new Object();
+        assertSame( val, ConfigUtils.getObject( config, val, "no-value" ) );
+    }
+
+    @Test
+    public void testGetObject_AlternativeKeys()
+    {
+        Object val = new Object();
+        config.put( "some-object", val );
+        assertSame( val, ConfigUtils.getObject( config, null, "no-object", "some-object" ) );
+    }
+
+    @Test
+    public void testGetMap_Default()
+    {
+        Map<?, ?> val = new HashMap<Object, Object>();
+        assertSame( val, ConfigUtils.getMap( config, val, "no-value" ) );
+    }
+
+    @Test
+    public void testGetMap_AlternativeKeys()
+    {
+        Map<?, ?> val = new HashMap<Object, Object>();
+        config.put( "some-map", val );
+        assertSame( val, ConfigUtils.getMap( config, null, "no-object", "some-map" ) );
+    }
+
+    @Test
+    public void testGetList_Default()
+    {
+        List<?> val = new ArrayList<Object>();
+        assertSame( val, ConfigUtils.getList( config, val, "no-value" ) );
+    }
+
+    @Test
+    public void testGetList_AlternativeKeys()
+    {
+        List<?> val = new ArrayList<Object>();
+        config.put( "some-list", val );
+        assertSame( val, ConfigUtils.getList( config, null, "no-object", "some-list" ) );
+    }
+
+    @Test
+    public void testGetList_CollectionConversion()
+    {
+        Collection<?> val = Collections.singleton( "item" );
+        config.put( "some-collection", val );
+        assertEquals( Arrays.asList( "item" ), ConfigUtils.getList( config, null, "some-collection" ) );
+    }
+
+    @Test
+    public void testGetString_Default()
+    {
+        config.put( "no-string", new Object() );
+        assertEquals( "default", ConfigUtils.getString( config, "default", "no-value" ) );
+        assertEquals( "default", ConfigUtils.getString( config, "default", "no-string" ) );
+    }
+
+    @Test
+    public void testGetString_AlternativeKeys()
+    {
+        config.put( "no-string", new Object() );
+        config.put( "some-string", "passed" );
+        assertEquals( "passed", ConfigUtils.getString( config, "default", "no-string", "some-string" ) );
+    }
+
+    @Test
+    public void testGetBoolean_Default()
+    {
+        config.put( "no-boolean", new Object() );
+        assertEquals( true, ConfigUtils.getBoolean( config, true, "no-value" ) );
+        assertEquals( false, ConfigUtils.getBoolean( config, false, "no-value" ) );
+        assertEquals( true, ConfigUtils.getBoolean( config, true, "no-boolean" ) );
+        assertEquals( false, ConfigUtils.getBoolean( config, false, "no-boolean" ) );
+    }
+
+    @Test
+    public void testGetBoolean_AlternativeKeys()
+    {
+        config.put( "no-boolean", new Object() );
+        config.put( "some-boolean", true );
+        assertEquals( true, ConfigUtils.getBoolean( config, false, "no-boolean", "some-boolean" ) );
+        config.put( "some-boolean", false );
+        assertEquals( false, ConfigUtils.getBoolean( config, true, "no-boolean", "some-boolean" ) );
+    }
+
+    @Test
+    public void testGetBoolean_StringConversion()
+    {
+        config.put( "some-boolean", "true" );
+        assertEquals( true, ConfigUtils.getBoolean( config, false, "some-boolean" ) );
+        config.put( "some-boolean", "false" );
+        assertEquals( false, ConfigUtils.getBoolean( config, true, "some-boolean" ) );
+    }
+
+    @Test
+    public void testGetInteger_Default()
+    {
+        config.put( "no-integer", new Object() );
+        assertEquals( -17, ConfigUtils.getInteger( config, -17, "no-value" ) );
+        assertEquals( 43, ConfigUtils.getInteger( config, 43, "no-integer" ) );
+    }
+
+    @Test
+    public void testGetInteger_AlternativeKeys()
+    {
+        config.put( "no-integer", "text" );
+        config.put( "some-integer", 23 );
+        assertEquals( 23, ConfigUtils.getInteger( config, 0, "no-integer", "some-integer" ) );
+    }
+
+    @Test
+    public void testGetInteger_StringConversion()
+    {
+        config.put( "some-integer", "-123456" );
+        assertEquals( -123456, ConfigUtils.getInteger( config, 0, "some-integer" ) );
+    }
+
+    @Test
+    public void testGetInteger_NumberConversion()
+    {
+        config.put( "some-number", -123456.789 );
+        assertEquals( -123456, ConfigUtils.getInteger( config, 0, "some-number" ) );
+    }
+
+    @Test
+    public void testGetLong_Default()
+    {
+        config.put( "no-long", new Object() );
+        assertEquals( -17L, ConfigUtils.getLong( config, -17L, "no-value" ) );
+        assertEquals( 43L, ConfigUtils.getLong( config, 43L, "no-long" ) );
+    }
+
+    @Test
+    public void testGetLong_AlternativeKeys()
+    {
+        config.put( "no-long", "text" );
+        config.put( "some-long", 23L );
+        assertEquals( 23L, ConfigUtils.getLong( config, 0, "no-long", "some-long" ) );
+    }
+
+    @Test
+    public void testGetLong_StringConversion()
+    {
+        config.put( "some-long", "-123456789012" );
+        assertEquals( -123456789012L, ConfigUtils.getLong( config, 0, "some-long" ) );
+    }
+
+    @Test
+    public void testGetLong_NumberConversion()
+    {
+        config.put( "some-number", -123456789012.789 );
+        assertEquals( -123456789012L, ConfigUtils.getLong( config, 0, "some-number" ) );
+    }
+
+    @Test
+    public void testGetFloat_Default()
+    {
+        config.put( "no-float", new Object() );
+        assertEquals( -17.1f, ConfigUtils.getFloat( config, -17.1f, "no-value" ), 0.01f );
+        assertEquals( 43.2f, ConfigUtils.getFloat( config, 43.2f, "no-float" ), 0.01f );
+    }
+
+    @Test
+    public void testGetFloat_AlternativeKeys()
+    {
+        config.put( "no-float", "text" );
+        config.put( "some-float", 12.3f );
+        assertEquals( 12.3f, ConfigUtils.getFloat( config, 0, "no-float", "some-float" ), 0.01f );
+    }
+
+    @Test
+    public void testGetFloat_StringConversion()
+    {
+        config.put( "some-float", "-12.3" );
+        assertEquals( -12.3f, ConfigUtils.getFloat( config, 0, "some-float" ), 0.01f );
+        config.put( "some-float", "NaN" );
+        assertEquals( true, Float.isNaN( ConfigUtils.getFloat( config, 0, "some-float" ) ) );
+    }
+
+    @Test
+    public void testGetFloat_NumberConversion()
+    {
+        config.put( "some-number", -1234f );
+        assertEquals( -1234f, ConfigUtils.getFloat( config, 0, "some-number" ), 0.1f );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringUtilsTest.java
new file mode 100644
index 0000000..4ac2f7e
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringUtilsTest.java
@@ -0,0 +1,41 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.util.StringUtils;
+import org.junit.Test;
+
+/**
+ */
+public class StringUtilsTest
+{
+
+    @Test
+    public void testIsEmpty()
+    {
+        assertTrue( StringUtils.isEmpty( null ) );
+        assertTrue( StringUtils.isEmpty( "" ) );
+        assertFalse( StringUtils.isEmpty( " " ) );
+        assertFalse( StringUtils.isEmpty( "test" ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/ArtifactIdUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/ArtifactIdUtilsTest.java
new file mode 100644
index 0000000..36193f3
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/ArtifactIdUtilsTest.java
@@ -0,0 +1,201 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.junit.Test;
+
+/**
+ */
+public class ArtifactIdUtilsTest
+{
+
+    @Test
+    public void testToIdArtifact()
+    {
+        Artifact artifact = null;
+        assertSame( null, ArtifactIdUtils.toId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( "gid:aid:ext:1.0-20110205.132618-23", ArtifactIdUtils.toId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "cls", "ext", "1.0-20110205.132618-23" );
+        assertEquals( "gid:aid:ext:cls:1.0-20110205.132618-23", ArtifactIdUtils.toId( artifact ) );
+    }
+
+    @Test
+    public void testToIdStrings()
+    {
+        assertEquals( ":::", ArtifactIdUtils.toId( null, null, null, null, null ) );
+
+        assertEquals( "gid:aid:ext:1", ArtifactIdUtils.toId( "gid", "aid", "ext", "", "1" ) );
+
+        assertEquals( "gid:aid:ext:cls:1", ArtifactIdUtils.toId( "gid", "aid", "ext", "cls", "1" ) );
+    }
+
+    @Test
+    public void testToBaseIdArtifact()
+    {
+        Artifact artifact = null;
+        assertSame( null, ArtifactIdUtils.toBaseId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( "gid:aid:ext:1.0-SNAPSHOT", ArtifactIdUtils.toBaseId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "cls", "ext", "1.0-20110205.132618-23" );
+        assertEquals( "gid:aid:ext:cls:1.0-SNAPSHOT", ArtifactIdUtils.toBaseId( artifact ) );
+    }
+
+    @Test
+    public void testToVersionlessIdArtifact()
+    {
+        Artifact artifact = null;
+        assertSame( null, ArtifactIdUtils.toId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "ext", "1" );
+        assertEquals( "gid:aid:ext", ArtifactIdUtils.toVersionlessId( artifact ) );
+
+        artifact = new DefaultArtifact( "gid", "aid", "cls", "ext", "1" );
+        assertEquals( "gid:aid:ext:cls", ArtifactIdUtils.toVersionlessId( artifact ) );
+    }
+
+    @Test
+    public void testToVersionlessIdStrings()
+    {
+        assertEquals( "::", ArtifactIdUtils.toVersionlessId( null, null, null, null ) );
+
+        assertEquals( "gid:aid:ext", ArtifactIdUtils.toVersionlessId( "gid", "aid", "ext", "" ) );
+
+        assertEquals( "gid:aid:ext:cls", ArtifactIdUtils.toVersionlessId( "gid", "aid", "ext", "cls" ) );
+    }
+
+    @Test
+    public void testEqualsId()
+    {
+        Artifact artifact1 = null;
+        Artifact artifact2 = null;
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact1 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gidX", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aidX", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "extX", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-24" );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( true, ArtifactIdUtils.equalsId( artifact1, artifact2 ) );
+        assertEquals( true, ArtifactIdUtils.equalsId( artifact2, artifact1 ) );
+
+        assertEquals( true, ArtifactIdUtils.equalsId( artifact1, artifact1 ) );
+    }
+
+    @Test
+    public void testEqualsBaseId()
+    {
+        Artifact artifact1 = null;
+        Artifact artifact2 = null;
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact1 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gidX", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aidX", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "extX", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "X.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-24" );
+        assertEquals( true, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( true, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( true, ArtifactIdUtils.equalsBaseId( artifact1, artifact2 ) );
+        assertEquals( true, ArtifactIdUtils.equalsBaseId( artifact2, artifact1 ) );
+
+        assertEquals( true, ArtifactIdUtils.equalsBaseId( artifact1, artifact1 ) );
+    }
+
+    @Test
+    public void testEqualsVersionlessId()
+    {
+        Artifact artifact1 = null;
+        Artifact artifact2 = null;
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact1 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gidX", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aidX", "ext", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "extX", "1.0-20110205.132618-23" );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( false, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-24" );
+        assertEquals( true, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( true, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        artifact2 = new DefaultArtifact( "gid", "aid", "ext", "1.0-20110205.132618-23" );
+        assertEquals( true, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact2 ) );
+        assertEquals( true, ArtifactIdUtils.equalsVersionlessId( artifact2, artifact1 ) );
+
+        assertEquals( true, ArtifactIdUtils.equalsVersionlessId( artifact1, artifact1 ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java
new file mode 100644
index 0000000..0ae333e
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java
@@ -0,0 +1,158 @@
+package org.eclipse.aether.util.artifact;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.util.artifact.SubArtifact;
+import org.junit.Test;
+
+/**
+ */
+public class SubArtifactTest
+{
+
+    private Artifact newMainArtifact( String coords )
+    {
+        return new DefaultArtifact( coords );
+    }
+
+    @Test
+    public void testMainArtifactFileNotRetained()
+    {
+        Artifact a = newMainArtifact( "gid:aid:ver" ).setFile( new File( "" ) );
+        assertNotNull( a.getFile() );
+        a = new SubArtifact( a, "", "pom" );
+        assertNull( a.getFile() );
+    }
+
+    @Test
+    public void testMainArtifactPropertiesNotRetained()
+    {
+        Artifact a = newMainArtifact( "gid:aid:ver" ).setProperties( Collections.singletonMap( "key", "value" ) );
+        assertEquals( 1, a.getProperties().size() );
+        a = new SubArtifact( a, "", "pom" );
+        assertEquals( 0, a.getProperties().size() );
+        assertSame( null, a.getProperty( "key", null ) );
+    }
+
+    @Test( expected = NullPointerException.class )
+    public void testMainArtifactMissing()
+    {
+        new SubArtifact( null, "", "pom" );
+    }
+
+    @Test
+    public void testEmptyClassifier()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "", "pom" );
+        assertEquals( "", sub.getClassifier() );
+        sub = new SubArtifact( main, null, "pom" );
+        assertEquals( "", sub.getClassifier() );
+    }
+
+    @Test
+    public void testEmptyExtension()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "tests", "" );
+        assertEquals( "", sub.getExtension() );
+        sub = new SubArtifact( main, "tests", null );
+        assertEquals( "", sub.getExtension() );
+    }
+
+    @Test
+    public void testSameClassifier()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "*", "pom" );
+        assertEquals( "cls", sub.getClassifier() );
+    }
+
+    @Test
+    public void testSameExtension()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "tests", "*" );
+        assertEquals( "ext", sub.getExtension() );
+    }
+
+    @Test
+    public void testDerivedClassifier()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "*-tests", "pom" );
+        assertEquals( "cls-tests", sub.getClassifier() );
+        sub = new SubArtifact( main, "tests-*", "pom" );
+        assertEquals( "tests-cls", sub.getClassifier() );
+
+        main = newMainArtifact( "gid:aid:ext:ver" );
+        sub = new SubArtifact( main, "*-tests", "pom" );
+        assertEquals( "tests", sub.getClassifier() );
+        sub = new SubArtifact( main, "tests-*", "pom" );
+        assertEquals( "tests", sub.getClassifier() );
+    }
+
+    @Test
+    public void testDerivedExtension()
+    {
+        Artifact main = newMainArtifact( "gid:aid:ext:cls:ver" );
+        Artifact sub = new SubArtifact( main, "", "*.asc" );
+        assertEquals( "ext.asc", sub.getExtension() );
+        sub = new SubArtifact( main, "", "asc.*" );
+        assertEquals( "asc.ext", sub.getExtension() );
+    }
+
+    @Test
+    public void testImmutability()
+    {
+        Artifact a = new SubArtifact( newMainArtifact( "gid:aid:ver" ), "", "pom" );
+        assertNotSame( a, a.setFile( new File( "file" ) ) );
+        assertNotSame( a, a.setVersion( "otherVersion" ) );
+        assertNotSame( a, a.setProperties( Collections.singletonMap( "key", "value" ) ) );
+    }
+
+    @Test
+    public void testPropertiesCopied()
+    {
+        Map<String, String> props = new HashMap<String, String>();
+        props.put( "key", "value1" );
+
+        Artifact a = new SubArtifact( newMainArtifact( "gid:aid:ver" ), "", "pom", props, null );
+        assertEquals( "value1", a.getProperty( "key", null ) );
+        props.clear();
+        assertEquals( "value1", a.getProperty( "key", null ) );
+
+        props.put( "key", "value2" );
+        a = a.setProperties( props );
+        assertEquals( "value2", a.getProperty( "key", null ) );
+        props.clear();
+        assertEquals( "value2", a.getProperty( "key", null ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AbstractDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AbstractDependencyFilterTest.java
new file mode 100644
index 0000000..835c1ce
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AbstractDependencyFilterTest.java
@@ -0,0 +1,55 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+
+public abstract class AbstractDependencyFilterTest
+{
+
+    protected DependencyFilter getAcceptFilter()
+    {
+        return new DependencyFilter()
+        {
+
+            public boolean accept( DependencyNode node, List<DependencyNode> parents )
+            {
+                return true;
+            }
+
+        };
+    }
+
+    protected DependencyFilter getDenyFilter()
+    {
+        return new DependencyFilter()
+        {
+
+            public boolean accept( DependencyNode node, List<DependencyNode> parents )
+            {
+                return false;
+            }
+        };
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AndDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AndDependencyFilterTest.java
new file mode 100644
index 0000000..45a7f3d
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/AndDependencyFilterTest.java
@@ -0,0 +1,92 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.AndDependencyFilter;
+import org.junit.Test;
+
+public class AndDependencyFilterTest
+    extends AbstractDependencyFilterTest
+{
+    @Test
+    public void acceptTest()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.artifactId( "test" );
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+
+        // Empty AND
+        assertTrue( new AndDependencyFilter().accept( builder.build(), parents ) );
+
+        // Basic Boolean Input
+        assertTrue( new AndDependencyFilter( getAcceptFilter() ).accept( builder.build(), parents ) );
+        assertFalse( new AndDependencyFilter( getDenyFilter() ).accept( builder.build(), parents ) );
+
+        assertFalse( new AndDependencyFilter( getDenyFilter(), getDenyFilter() ).accept( builder.build(), parents ) );
+        assertFalse( new AndDependencyFilter( getDenyFilter(), getAcceptFilter() ).accept( builder.build(), parents ) );
+        assertFalse( new AndDependencyFilter( getAcceptFilter(), getDenyFilter() ).accept( builder.build(), parents ) );
+        assertTrue( new AndDependencyFilter( getAcceptFilter(), getAcceptFilter() ).accept( builder.build(), parents ) );
+
+        assertFalse( new AndDependencyFilter( getDenyFilter(), getDenyFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                          parents ) );
+        assertFalse( new AndDependencyFilter( getAcceptFilter(), getDenyFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                            parents ) );
+        assertFalse( new AndDependencyFilter( getAcceptFilter(), getAcceptFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                              parents ) );
+        assertTrue( new AndDependencyFilter( getAcceptFilter(), getAcceptFilter(), getAcceptFilter() ).accept( builder.build(),
+                                                                                                               parents ) );
+
+        // User another constructor
+        Collection<DependencyFilter> filters = new LinkedList<DependencyFilter>();
+        filters.add( getDenyFilter() );
+        filters.add( getAcceptFilter() );
+        assertFalse( new AndDependencyFilter( filters ).accept( builder.build(), parents ) );
+
+        filters = new LinkedList<DependencyFilter>();
+        filters.add( getDenyFilter() );
+        filters.add( getDenyFilter() );
+        assertFalse( new AndDependencyFilter( filters ).accept( builder.build(), parents ) );
+
+        filters = new LinkedList<DependencyFilter>();
+        filters.add( getAcceptFilter() );
+        filters.add( getAcceptFilter() );
+        assertTrue( new AndDependencyFilter( filters ).accept( builder.build(), parents ) );
+
+        // newInstance
+        assertTrue( AndDependencyFilter.newInstance( getAcceptFilter(), getAcceptFilter() ).accept( builder.build(),
+                                                                                                    parents ) );
+        assertFalse( AndDependencyFilter.newInstance( getAcceptFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                   parents ) );
+
+        assertFalse( AndDependencyFilter.newInstance( getDenyFilter(), null ).accept( builder.build(), parents ) );
+        assertTrue( AndDependencyFilter.newInstance( getAcceptFilter(), null ).accept( builder.build(), parents ) );
+        assertNull( AndDependencyFilter.newInstance( null, null ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/DependencyFilterUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/DependencyFilterUtilsTest.java
new file mode 100644
index 0000000..28bde57
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/DependencyFilterUtilsTest.java
@@ -0,0 +1,141 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.DependencyFilterUtils;
+import org.junit.Test;
+
+/**
+ */
+public class DependencyFilterUtilsTest
+{
+
+    private static List<DependencyNode> PARENTS = Collections.emptyList();
+
+    @Test
+    public void testClasspathFilterCompile()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "compile" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterRuntime()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "runtime" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterTest()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "test" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterCompileRuntime()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "compile", "runtime" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterCompilePlusRuntime()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "compile+runtime" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterRuntimeCommaSystem()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "runtime,system" );
+
+        assertTrue( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterNull()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( (String[]) null );
+
+        assertFalse( filter.accept( builder.scope( "compile" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "system" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "provided" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "runtime" ).build(), PARENTS ) );
+        assertFalse( filter.accept( builder.scope( "test" ).build(), PARENTS ) );
+    }
+
+    @Test
+    public void testClasspathFilterUnknownScope()
+    {
+        NodeBuilder builder = new NodeBuilder().artifactId( "aid" );
+        DependencyFilter filter = DependencyFilterUtils.classpathFilter( "compile" );
+
+        assertTrue( filter.accept( builder.scope( "" ).build(), PARENTS ) );
+        assertTrue( filter.accept( builder.scope( "unknown" ).build(), PARENTS ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ExclusionDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ExclusionDependencyFilterTest.java
new file mode 100644
index 0000000..a0be592
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ExclusionDependencyFilterTest.java
@@ -0,0 +1,60 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.ExclusionsDependencyFilter;
+import org.junit.Test;
+
+public class ExclusionDependencyFilterTest
+{
+
+    @Test
+    public void acceptTest()
+    {
+
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" );
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+        String[] excludes;
+
+        excludes = new String[] { "com.example.test:testArtifact" };
+        assertFalse( new ExclusionsDependencyFilter( Arrays.asList( excludes ) ).accept( builder.build(), parents ) );
+
+        excludes = new String[] { "com.example.test:testArtifact", "com.foo:otherArtifact" };
+        assertFalse( new ExclusionsDependencyFilter( Arrays.asList( excludes ) ).accept( builder.build(), parents ) );
+
+        excludes = new String[] { "testArtifact" };
+        assertFalse( new ExclusionsDependencyFilter( Arrays.asList( excludes ) ).accept( builder.build(), parents ) );
+
+        excludes = new String[] { "otherArtifact" };
+        assertTrue( new ExclusionsDependencyFilter( Arrays.asList( excludes ) ).accept( builder.build(), parents ) );
+
+        assertTrue( new ExclusionsDependencyFilter( (Collection<String>) null ).accept( builder.build(), parents ) );
+    }
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/OrDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/OrDependencyFilterTest.java
new file mode 100644
index 0000000..03b80ea
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/OrDependencyFilterTest.java
@@ -0,0 +1,87 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.AndDependencyFilter;
+import org.eclipse.aether.util.filter.OrDependencyFilter;
+import org.junit.Test;
+
+public class OrDependencyFilterTest
+    extends AbstractDependencyFilterTest
+{
+
+    @Test
+    public void acceptTest()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.artifactId( "test" );
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+        // Empty OR
+        assertFalse( new OrDependencyFilter().accept( builder.build(), parents ) );
+
+        // Basic Boolean Input
+        assertTrue( new OrDependencyFilter( getAcceptFilter() ).accept( builder.build(), parents ) );
+        assertFalse( new OrDependencyFilter( getDenyFilter() ).accept( builder.build(), parents ) );
+
+        assertFalse( new OrDependencyFilter( getDenyFilter(), getDenyFilter() ).accept( builder.build(), parents ) );
+        assertTrue( new OrDependencyFilter( getDenyFilter(), getAcceptFilter() ).accept( builder.build(), parents ) );
+        assertTrue( new OrDependencyFilter( getAcceptFilter(), getDenyFilter() ).accept( builder.build(), parents ) );
+        assertTrue( new OrDependencyFilter( getAcceptFilter(), getAcceptFilter() ).accept( builder.build(), parents ) );
+
+        assertFalse( new OrDependencyFilter( getDenyFilter(), getDenyFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                         parents ) );
+        assertTrue( new OrDependencyFilter( getAcceptFilter(), getDenyFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                          parents ) );
+        assertTrue( new OrDependencyFilter( getAcceptFilter(), getAcceptFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                            parents ) );
+        assertTrue( new OrDependencyFilter( getAcceptFilter(), getAcceptFilter(), getAcceptFilter() ).accept( builder.build(),
+                                                                                                              parents ) );
+
+        // User another constructor
+        Collection<DependencyFilter> filters = new LinkedList<DependencyFilter>();
+        filters.add( getDenyFilter() );
+        filters.add( getAcceptFilter() );
+        assertTrue( new OrDependencyFilter( filters ).accept( builder.build(), parents ) );
+
+        filters = new LinkedList<DependencyFilter>();
+        filters.add( getDenyFilter() );
+        filters.add( getDenyFilter() );
+        assertFalse( new OrDependencyFilter( filters ).accept( builder.build(), parents ) );
+
+        // newInstance
+        assertTrue( AndDependencyFilter.newInstance( getAcceptFilter(), getAcceptFilter() ).accept( builder.build(),
+                                                                                                    parents ) );
+        assertFalse( AndDependencyFilter.newInstance( getAcceptFilter(), getDenyFilter() ).accept( builder.build(),
+                                                                                                   parents ) );
+        assertTrue( AndDependencyFilter.newInstance( getAcceptFilter(), null ).accept( builder.build(), parents ) );
+        assertFalse( AndDependencyFilter.newInstance( getDenyFilter(), null ).accept( builder.build(), parents ) );
+        assertNull( AndDependencyFilter.newInstance( null, null ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilterTest.java
new file mode 100644
index 0000000..b5b307e
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternExclusionsDependencyFilterTest.java
@@ -0,0 +1,187 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.VersionScheme;
+import org.junit.Test;
+
+public class PatternExclusionsDependencyFilterTest
+{
+
+    @Test
+    public void acceptTestCornerCases()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.artifactId( "testArtifact" );
+        DependencyNode node = builder.build();
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+
+        // Empty String, Empty List
+        assertTrue( dontAccept( node, "" ) );
+        assertTrue( new PatternExclusionsDependencyFilter( new LinkedList<String>() ).accept( node, parents ) );
+        assertTrue( new PatternExclusionsDependencyFilter( (String[]) null ).accept( node, parents ) );
+        assertTrue( new PatternExclusionsDependencyFilter( (VersionScheme) null, "[1,10]" ).accept( node, parents ) );
+    }
+
+    @Test
+    public void acceptTestMatches()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        // full match
+        assertEquals( "com.example.test:testArtifact:jar:1.0.3", true,
+                      dontAccept( node, "com.example.test:testArtifact:jar:1.0.3" ) );
+
+        // single wildcard
+        assertEquals( "*:testArtifact:jar:1.0.3", true, dontAccept( node, "*:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:*:jar:1.0.3", true, dontAccept( node, "com.example.test:*:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:1.0.3", true,
+                      dontAccept( node, "com.example.test:testArtifact:*:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:1.0.3", true,
+                      dontAccept( node, "com.example.test:testArtifact:*:1.0.3" ) );
+
+        // implicit wildcard
+        assertEquals( ":testArtifact:jar:1.0.3", true, dontAccept( node, ":testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test::jar:1.0.3", true, dontAccept( node, "com.example.test::jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact::1.0.3", true,
+                      dontAccept( node, "com.example.test:testArtifact::1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:jar:", true,
+                      dontAccept( node, "com.example.test:testArtifact:jar:" ) );
+
+        // multi wildcards
+        assertEquals( "*:*:jar:1.0.3", true, dontAccept( node, "*:*:jar:1.0.3" ) );
+        assertEquals( "com.example.test:*:*:1.0.3", true, dontAccept( node, "com.example.test:*:*:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:*", true, dontAccept( node, "com.example.test:testArtifact:*:*" ) );
+        assertEquals( "*:testArtifact:jar:*", true, dontAccept( node, "*:testArtifact:jar:*" ) );
+        assertEquals( "*:*:jar:*", true, dontAccept( node, "*:*:jar:*" ) );
+        assertEquals( ":*:jar:", true, dontAccept( node, ":*:jar:" ) );
+
+        // partial wildcards
+        assertEquals( "*.example.test:testArtifact:jar:1.0.3", true,
+                      dontAccept( node, "*.example.test:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*ar:1.0.*", true,
+                      dontAccept( node, "com.example.test:testArtifact:*ar:1.0.*" ) );
+        assertEquals( "com.example.test:testArtifact:jar:1.0.*", true,
+                      dontAccept( node, "com.example.test:testArtifact:jar:1.0.*" ) );
+        assertEquals( "*.example.*:testArtifact:jar:1.0.3", true,
+                      dontAccept( node, "*.example.*:testArtifact:jar:1.0.3" ) );
+
+        // wildcard as empty string
+        assertEquals( "com.example.test*:testArtifact:jar:1.0.3", true,
+                      dontAccept( node, "com.example.test*:testArtifact:jar:1.0.3" ) );
+    }
+
+    @Test
+    public void acceptTestLessToken()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        assertEquals( "com.example.test:testArtifact:jar", true, dontAccept( node, "com.example.test:testArtifact:jar" ) );
+        assertEquals( "com.example.test:testArtifact", true, dontAccept( node, "com.example.test:testArtifact" ) );
+        assertEquals( "com.example.test", true, dontAccept( node, "com.example.test" ) );
+
+        assertEquals( "com.example.foo", false, dontAccept( node, "com.example.foo" ) );
+    }
+
+    @Test
+    public void acceptTestMissmatch()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        assertEquals( "OTHER.GROUP.ID:testArtifact:jar:1.0.3", false,
+                      dontAccept( node, "OTHER.GROUP.ID:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:OTHER_ARTIFACT:jar:1.0.3", false,
+                      dontAccept( node, "com.example.test:OTHER_ARTIFACT:jar:1.0.3" ) );
+        assertEquals( "com.example.test:OTHER_ARTIFACT:jar:1.0.3", false,
+                      dontAccept( node, "com.example.test:OTHER_ARTIFACT:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:WAR:1.0.3", false,
+                      dontAccept( node, "com.example.test:testArtifact:WAR:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:jar:SNAPSHOT", false,
+                      dontAccept( node, "com.example.test:testArtifact:jar:SNAPSHOT" ) );
+
+        assertEquals( "*:*:war:*", false, dontAccept( node, "*:*:war:*" ) );
+        assertEquals( "OTHER.GROUP.ID", false, dontAccept( node, "OTHER.GROUP.ID" ) );
+    }
+
+    @Test
+    public void acceptTestMoreToken()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+
+        DependencyNode node = builder.build();
+        assertEquals( "com.example.test:testArtifact:jar:1.0.3:foo", false,
+                      dontAccept( node, "com.example.test:testArtifact:jar:1.0.3:foo" ) );
+    }
+
+    @Test
+    public void acceptTestRange()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        String prefix = "com.example.test:testArtifact:jar:";
+
+        assertTrue( prefix + "[1.0.3,1.0.4)", dontAcceptVersionRange( node, prefix + "[1.0.3,1.0.4)" ) );
+        assertTrue( prefix + "[1.0.3,)", dontAcceptVersionRange( node, prefix + "[1.0.3,)" ) );
+        assertTrue( prefix + "[1.0.3,]", dontAcceptVersionRange( node, prefix + "[1.0.3,]" ) );
+        assertTrue( prefix + "(,1.0.3]", dontAcceptVersionRange( node, prefix + "(,1.0.3]" ) );
+        assertTrue( prefix + "[1.0,]", dontAcceptVersionRange( node, prefix + "[1.0,]" ) );
+        assertTrue( prefix + "[1,4]", dontAcceptVersionRange( node, prefix + "[1,4]" ) );
+        assertTrue( prefix + "(1,4)", dontAcceptVersionRange( node, prefix + "(1,4)" ) );
+
+        assertTrue( prefix + "(1.0.2,1.0.3]",
+                    dontAcceptVersionRange( node, prefix + "(1.0.2,1.0.3]", prefix + "(1.1,)" ) );
+
+        assertFalse( prefix + "(1.0.3,2.0]", dontAcceptVersionRange( node, prefix + "(1.0.3,2.0]" ) );
+        assertFalse( prefix + "(1,1.0.2]", dontAcceptVersionRange( node, prefix + "(1,1.0.2]" ) );
+
+        assertFalse( prefix + "(1.0.2,1.0.3)",
+                     dontAcceptVersionRange( node, prefix + "(1.0.2,1.0.3)", prefix + "(1.0.3,)" ) );
+    }
+
+    private boolean dontAccept( DependencyNode node, String expression )
+    {
+        return !new PatternExclusionsDependencyFilter( expression ).accept( node, new LinkedList<DependencyNode>() );
+    }
+
+    private boolean dontAcceptVersionRange( DependencyNode node, String... expression )
+    {
+        return !new PatternExclusionsDependencyFilter( new GenericVersionScheme(), expression ).accept( node,
+                                                                                                        new LinkedList<DependencyNode>() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilterTest.java
new file mode 100644
index 0000000..cb85431
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/PatternInclusionsDependencyFilterTest.java
@@ -0,0 +1,184 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.PatternInclusionsDependencyFilter;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.VersionScheme;
+import org.junit.Test;
+
+public class PatternInclusionsDependencyFilterTest
+    extends AbstractDependencyFilterTest
+{
+
+    @Test
+    public void acceptTestCornerCases()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.artifactId( "testArtifact" );
+        DependencyNode node = builder.build();
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+
+        // Empty String, Empty List
+        assertTrue( accept( node, "" ) );
+        assertFalse( new PatternInclusionsDependencyFilter( new LinkedList<String>() ).accept( node, parents ) );
+        assertFalse( new PatternInclusionsDependencyFilter( (String[]) null ).accept( node, parents ) );
+        assertFalse( new PatternInclusionsDependencyFilter( (VersionScheme) null, "[1,10]" ).accept( node, parents ) );
+    }
+
+    @Test
+    public void acceptTestMatches()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        // full match
+        assertEquals( "com.example.test:testArtifact:jar:1.0.3", true,
+                      accept( node, "com.example.test:testArtifact:jar:1.0.3" ) );
+
+        // single wildcard
+        assertEquals( "*:testArtifact:jar:1.0.3", true, accept( node, "*:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:*:jar:1.0.3", true, accept( node, "com.example.test:*:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:1.0.3", true,
+                      accept( node, "com.example.test:testArtifact:*:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:1.0.3", true,
+                      accept( node, "com.example.test:testArtifact:*:1.0.3" ) );
+
+        // implicit wildcard
+        assertEquals( ":testArtifact:jar:1.0.3", true, accept( node, ":testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test::jar:1.0.3", true, accept( node, "com.example.test::jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact::1.0.3", true,
+                      accept( node, "com.example.test:testArtifact::1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:jar:", true, accept( node, "com.example.test:testArtifact:jar:" ) );
+
+        // multi wildcards
+        assertEquals( "*:*:jar:1.0.3", true, accept( node, "*:*:jar:1.0.3" ) );
+        assertEquals( "com.example.test:*:*:1.0.3", true, accept( node, "com.example.test:*:*:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*:*", true, accept( node, "com.example.test:testArtifact:*:*" ) );
+        assertEquals( "*:testArtifact:jar:*", true, accept( node, "*:testArtifact:jar:*" ) );
+        assertEquals( "*:*:jar:*", true, accept( node, "*:*:jar:*" ) );
+        assertEquals( ":*:jar:", true, accept( node, ":*:jar:" ) );
+
+        // partial wildcards
+        assertEquals( "*.example.test:testArtifact:jar:1.0.3", true,
+                      accept( node, "*.example.test:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:*ar:1.0.*", true,
+                      accept( node, "com.example.test:testArtifact:*ar:1.0.*" ) );
+        assertEquals( "com.example.test:testArtifact:jar:1.0.*", true,
+                      accept( node, "com.example.test:testArtifact:jar:1.0.*" ) );
+        assertEquals( "*.example.*:testArtifact:jar:1.0.3", true, accept( node, "*.example.*:testArtifact:jar:1.0.3" ) );
+
+        // wildcard as empty string
+        assertEquals( "com.example.test*:testArtifact:jar:1.0.3", true,
+                      accept( node, "com.example.test*:testArtifact:jar:1.0.3" ) );
+    }
+
+    @Test
+    public void acceptTestLessToken()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        assertEquals( "com.example.test:testArtifact:jar", true, accept( node, "com.example.test:testArtifact:jar" ) );
+        assertEquals( "com.example.test:testArtifact", true, accept( node, "com.example.test:testArtifact" ) );
+        assertEquals( "com.example.test", true, accept( node, "com.example.test" ) );
+
+        assertEquals( "com.example.foo", false, accept( node, "com.example.foo" ) );
+    }
+
+    @Test
+    public void acceptTestMissmatch()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        assertEquals( "OTHER.GROUP.ID:testArtifact:jar:1.0.3", false,
+                      accept( node, "OTHER.GROUP.ID:testArtifact:jar:1.0.3" ) );
+        assertEquals( "com.example.test:OTHER_ARTIFACT:jar:1.0.3", false,
+                      accept( node, "com.example.test:OTHER_ARTIFACT:jar:1.0.3" ) );
+        assertEquals( "com.example.test:OTHER_ARTIFACT:jar:1.0.3", false,
+                      accept( node, "com.example.test:OTHER_ARTIFACT:jar:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:WAR:1.0.3", false,
+                      accept( node, "com.example.test:testArtifact:WAR:1.0.3" ) );
+        assertEquals( "com.example.test:testArtifact:jar:SNAPSHOT", false,
+                      accept( node, "com.example.test:testArtifact:jar:SNAPSHOT" ) );
+
+        assertEquals( "*:*:war:*", false, accept( node, "*:*:war:*" ) );
+        assertEquals( "OTHER.GROUP.ID", false, accept( node, "OTHER.GROUP.ID" ) );
+    }
+
+    @Test
+    public void acceptTestMoreToken()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+
+        DependencyNode node = builder.build();
+        assertEquals( "com.example.test:testArtifact:jar:1.0.3:foo", false,
+                      accept( node, "com.example.test:testArtifact:jar:1.0.3:foo" ) );
+    }
+
+    @Test
+    public void acceptTestRange()
+    {
+        NodeBuilder builder = new NodeBuilder();
+        builder.groupId( "com.example.test" ).artifactId( "testArtifact" ).ext( "jar" ).version( "1.0.3" );
+        DependencyNode node = builder.build();
+
+        String prefix = "com.example.test:testArtifact:jar:";
+
+        assertTrue( prefix + "[1.0.3,1.0.4)", acceptVersionRange( node, prefix + "[1.0.3,1.0.4)" ) );
+        assertTrue( prefix + "[1.0.3,)", acceptVersionRange( node, prefix + "[1.0.3,)" ) );
+        assertTrue( prefix + "[1.0.3,]", acceptVersionRange( node, prefix + "[1.0.3,]" ) );
+        assertTrue( prefix + "(,1.0.3]", acceptVersionRange( node, prefix + "(,1.0.3]" ) );
+        assertTrue( prefix + "[1.0,]", acceptVersionRange( node, prefix + "[1.0,]" ) );
+        assertTrue( prefix + "[1,4]", acceptVersionRange( node, prefix + "[1,4]" ) );
+        assertTrue( prefix + "(1,4)", acceptVersionRange( node, prefix + "(1,4)" ) );
+
+        assertTrue( prefix + "(1.0.2,1.0.3]", acceptVersionRange( node, prefix + "(1.0.2,1.0.3]", prefix + "(1.1,)" ) );
+
+        assertFalse( prefix + "(1.0.3,2.0]", acceptVersionRange( node, prefix + "(1.0.3,2.0]" ) );
+        assertFalse( prefix + "(1,1.0.2]", acceptVersionRange( node, prefix + "(1,1.0.2]" ) );
+
+        assertFalse( prefix + "(1.0.2,1.0.3)", acceptVersionRange( node, prefix + "(1.0.2,1.0.3)", prefix + "(1.0.3,)" ) );
+    }
+
+    public boolean accept( DependencyNode node, String expression )
+    {
+        return new PatternInclusionsDependencyFilter( expression ).accept( node, new LinkedList<DependencyNode>() );
+    }
+
+    public boolean acceptVersionRange( DependencyNode node, String... expression )
+    {
+        return new PatternInclusionsDependencyFilter( new GenericVersionScheme(), expression ).accept( node,
+                                                                                                       new LinkedList<DependencyNode>() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ScopeDependencyFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ScopeDependencyFilterTest.java
new file mode 100644
index 0000000..e943df9
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/filter/ScopeDependencyFilterTest.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.util.filter;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.NodeBuilder;
+import org.eclipse.aether.util.filter.ScopeDependencyFilter;
+import org.junit.Test;
+
+public class ScopeDependencyFilterTest
+    extends AbstractDependencyFilterTest
+{
+
+    @Test
+    public void acceptTest()
+    {
+
+        NodeBuilder builder = new NodeBuilder();
+        builder.scope( "compile" ).artifactId( "test" );
+        List<DependencyNode> parents = new LinkedList<DependencyNode>();
+
+        // null or empty
+        assertTrue( new ScopeDependencyFilter( null, null ).accept( builder.build(), parents ) );
+        assertTrue( new ScopeDependencyFilter( new LinkedList<String>(), new LinkedList<String>() ).accept( builder.build(),
+                                                                                                            parents ) );
+        assertTrue( new ScopeDependencyFilter( (String[]) null ).accept( builder.build(), parents ) );
+
+        // only excludes
+        assertTrue( new ScopeDependencyFilter( "test" ).accept( builder.build(), parents ) );
+        assertFalse( new ScopeDependencyFilter( "compile" ).accept( builder.build(), parents ) );
+        assertFalse( new ScopeDependencyFilter( "compile", "test" ).accept( builder.build(), parents ) );
+
+        // Both
+        String[] excludes1 = { "provided" };
+        String[] includes1 = { "compile", "test" };
+        assertTrue( new ScopeDependencyFilter( Arrays.asList( includes1 ), Arrays.asList( excludes1 ) ).accept( builder.build(),
+                                                                                                                parents ) );
+        assertTrue( new ScopeDependencyFilter( Arrays.asList( includes1 ), null ).accept( builder.build(), parents ) );
+
+        // exclude wins
+        String[] excludes2 = { "compile" };
+        String[] includes2 = { "compile" };
+        assertFalse( new ScopeDependencyFilter( Arrays.asList( includes2 ), Arrays.asList( excludes2 ) ).accept( builder.build(),
+                                                                                                                 parents ) );
+
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManagerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManagerTest.java
new file mode 100644
index 0000000..2593585
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/ClassicDependencyManagerTest.java
@@ -0,0 +1,82 @@
+package org.eclipse.aether.util.graph.manager;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ClassicDependencyManagerTest
+{
+
+    private final Artifact A = new DefaultArtifact( "test", "a", "", "" );
+
+    private final Artifact A1 = new DefaultArtifact( "test", "a", "", "1" );
+
+    private final Artifact B = new DefaultArtifact( "test", "b", "", "" );
+
+    private final Artifact B1 = new DefaultArtifact( "test", "b", "", "1" );
+
+    private RepositorySystemSession session;
+
+    private DependencyCollectionContext newContext( Dependency... managedDependencies )
+    {
+        return TestUtils.newCollectionContext( session, null, Arrays.asList( managedDependencies ) );
+    }
+
+    @Before
+    public void setUp()
+    {
+        session = TestUtils.newSession();
+    }
+
+    @Test
+    public void testManageOptional()
+    {
+        DependencyManager manager = new ClassicDependencyManager();
+
+        manager =
+            manager.deriveChildManager( newContext( new Dependency( A, null, null ), new Dependency( B, null, true ) ) );
+        DependencyManagement mngt;
+        mngt = manager.manageDependency( new Dependency( A1, null ) );
+        assertNull( mngt );
+        mngt = manager.manageDependency( new Dependency( B1, null ) );
+        assertNull( mngt );
+
+        manager = manager.deriveChildManager( newContext() );
+        mngt = manager.manageDependency( new Dependency( A1, null ) );
+        assertNull( mngt );
+        mngt = manager.manageDependency( new Dependency( B1, null ) );
+        assertNotNull( mngt );
+        assertEquals( Boolean.TRUE, mngt.getOptional() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/selector/AndDependencySelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/selector/AndDependencySelectorTest.java
new file mode 100644
index 0000000..b0f2b09
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/selector/AndDependencySelectorTest.java
@@ -0,0 +1,153 @@
+package org.eclipse.aether.util.graph.selector;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+import org.junit.Test;
+
+public class AndDependencySelectorTest
+{
+
+    static class DummyDependencySelector
+        implements DependencySelector
+    {
+
+        private final boolean select;
+
+        private final DependencySelector child;
+
+        public DummyDependencySelector()
+        {
+            this( true );
+        }
+
+        public DummyDependencySelector( boolean select )
+        {
+            this.select = select;
+            this.child = this;
+        }
+
+        public DummyDependencySelector( boolean select, DependencySelector child )
+        {
+            this.select = select;
+            this.child = child;
+        }
+
+        public boolean selectDependency( Dependency dependency )
+        {
+            return select;
+        }
+
+        public DependencySelector deriveChildSelector( DependencyCollectionContext context )
+        {
+            return child;
+        }
+
+    }
+
+    @Test
+    public void testNewInstance()
+    {
+        assertNull( AndDependencySelector.newInstance( null, null ) );
+        DependencySelector selector = new DummyDependencySelector();
+        assertSame( selector, AndDependencySelector.newInstance( selector, null ) );
+        assertSame( selector, AndDependencySelector.newInstance( null, selector ) );
+        assertSame( selector, AndDependencySelector.newInstance( selector, selector ) );
+        assertNotNull( AndDependencySelector.newInstance( selector, new DummyDependencySelector() ) );
+    }
+
+    @Test
+    public void testTraverseDependency()
+    {
+        Dependency dependency = new Dependency( new DefaultArtifact( "g:a:v:1" ), "runtime" );
+
+        DependencySelector selector = new AndDependencySelector();
+        assertTrue( selector.selectDependency( dependency ) );
+
+        selector =
+            new AndDependencySelector( new DummyDependencySelector( false ), new DummyDependencySelector( false ) );
+        assertFalse( selector.selectDependency( dependency ) );
+
+        selector =
+            new AndDependencySelector( new DummyDependencySelector( true ), new DummyDependencySelector( false ) );
+        assertFalse( selector.selectDependency( dependency ) );
+
+        selector = new AndDependencySelector( new DummyDependencySelector( true ), new DummyDependencySelector( true ) );
+        assertTrue( selector.selectDependency( dependency ) );
+    }
+
+    @Test
+    public void testDeriveChildSelector_Unchanged()
+    {
+        DependencySelector other1 = new DummyDependencySelector( true );
+        DependencySelector other2 = new DummyDependencySelector( false );
+        DependencySelector selector = new AndDependencySelector( other1, other2 );
+        assertSame( selector, selector.deriveChildSelector( null ) );
+    }
+
+    @Test
+    public void testDeriveChildSelector_OneRemaining()
+    {
+        DependencySelector other1 = new DummyDependencySelector( true );
+        DependencySelector other2 = new DummyDependencySelector( false, null );
+        DependencySelector selector = new AndDependencySelector( other1, other2 );
+        assertSame( other1, selector.deriveChildSelector( null ) );
+    }
+
+    @Test
+    public void testDeriveChildSelector_ZeroRemaining()
+    {
+        DependencySelector other1 = new DummyDependencySelector( true, null );
+        DependencySelector other2 = new DummyDependencySelector( false, null );
+        DependencySelector selector = new AndDependencySelector( other1, other2 );
+        assertNull( selector.deriveChildSelector( null ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        DependencySelector other1 = new DummyDependencySelector( true );
+        DependencySelector other2 = new DummyDependencySelector( false );
+        DependencySelector selector1 = new AndDependencySelector( other1, other2 );
+        DependencySelector selector2 = new AndDependencySelector( other2, other1 );
+        DependencySelector selector3 = new AndDependencySelector( other1 );
+        assertEquals( selector1, selector1 );
+        assertEquals( selector1, selector2 );
+        assertNotEquals( selector1, selector3 );
+        assertNotEquals( selector1, this );
+        assertNotEquals( selector1, null );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        DependencySelector other1 = new DummyDependencySelector( true );
+        DependencySelector other2 = new DummyDependencySelector( false );
+        DependencySelector selector1 = new AndDependencySelector( other1, other2 );
+        DependencySelector selector2 = new AndDependencySelector( other2, other1 );
+        assertEquals( selector1.hashCode(), selector2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/AbstractDependencyGraphTransformerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/AbstractDependencyGraphTransformerTest.java
new file mode 100644
index 0000000..b5947ed
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/AbstractDependencyGraphTransformerTest.java
@@ -0,0 +1,131 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ */
+public abstract class AbstractDependencyGraphTransformerTest
+{
+
+    protected DependencyGraphTransformer transformer;
+
+    protected DependencyGraphParser parser;
+
+    protected DefaultRepositorySystemSession session;
+
+    protected DependencyGraphTransformationContext context;
+
+    protected abstract DependencyGraphTransformer newTransformer();
+
+    protected abstract DependencyGraphParser newParser();
+
+    protected DependencyNode transform( DependencyNode root )
+        throws Exception
+    {
+        context = TestUtils.newTransformationContext( session );
+        root = transformer.transformGraph( root, context );
+        assertNotNull( root );
+        return root;
+    }
+
+    protected DependencyNode parseResource( String resource, String... substitutions )
+        throws Exception
+    {
+        parser.setSubstitutions( substitutions );
+        return parser.parseResource( resource );
+    }
+
+    protected DependencyNode parseLiteral( String literal, String... substitutions )
+        throws Exception
+    {
+        parser.setSubstitutions( substitutions );
+        return parser.parseLiteral( literal );
+    }
+
+    protected List<DependencyNode> find( DependencyNode node, String id )
+    {
+        LinkedList<DependencyNode> trail = new LinkedList<DependencyNode>();
+        find( trail, node, id );
+        return trail;
+    }
+
+    private boolean find( LinkedList<DependencyNode> trail, DependencyNode node, String id )
+    {
+        trail.addFirst( node );
+
+        if ( isMatch( node, id ) )
+        {
+            return true;
+        }
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            if ( find( trail, child, id ) )
+            {
+                return true;
+            }
+        }
+
+        trail.removeFirst();
+
+        return false;
+    }
+
+    private boolean isMatch( DependencyNode node, String id )
+    {
+        if ( node.getDependency() == null )
+        {
+            return false;
+        }
+        return id.equals( node.getDependency().getArtifact().getArtifactId() );
+    }
+
+    @Before
+    public void setUp()
+    {
+        transformer = newTransformer();
+        parser = newParser();
+        session = new DefaultRepositorySystemSession();
+    }
+
+    @After
+    public void tearDown()
+    {
+        transformer = null;
+        parser = null;
+        session = null;
+        context = null;
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorterTest.java
new file mode 100644
index 0000000..b24a920
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictIdSorterTest.java
@@ -0,0 +1,129 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.util.graph.transformer.ConflictIdSorter;
+import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
+import org.junit.Test;
+
+/**
+ */
+public class ConflictIdSorterTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    @Override
+    protected DependencyGraphTransformer newTransformer()
+    {
+        return new ChainedDependencyGraphTransformer( new SimpleConflictMarker(), new ConflictIdSorter() );
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/conflict-id-sorter/" );
+    }
+
+    private void expectOrder( List<String> sorted, String... ids )
+    {
+        Queue<String> queue = new LinkedList<String>( sorted );
+
+        for ( String id : ids )
+        {
+            String item = queue.poll();
+            assertNotNull( String.format( "not enough conflict groups (no match for '%s'", id ), item );
+
+            if ( !"*".equals( id ) )
+            {
+                assertEquals( id, item );
+            }
+        }
+
+        assertTrue( String.format( "leftover conflict groups (remaining: '%s')", queue ), queue.isEmpty() );
+    }
+
+    private void expectOrder( String... id )
+    {
+        @SuppressWarnings( "unchecked" )
+        List<String> sorted = (List<String>) context.get( TransformationContextKeys.SORTED_CONFLICT_IDS );
+        expectOrder( sorted, id );
+    }
+
+    private void expectCycle( boolean cycle )
+    {
+        Collection<?> cycles = (Collection<?>) context.get( TransformationContextKeys.CYCLIC_CONFLICT_IDS );
+        assertEquals( cycle, !cycles.isEmpty() );
+    }
+
+    @Test
+    public void testSimple()
+        throws Exception
+    {
+        DependencyNode node = parseResource( "simple.txt" );
+        assertSame( node, transform( node ) );
+
+        expectOrder( "gid2:aid::jar", "gid:aid::jar", "gid:aid2::jar" );
+        expectCycle( false );
+    }
+
+    @Test
+    public void testCycle()
+        throws Exception
+    {
+        DependencyNode node = parseResource( "cycle.txt" );
+        assertSame( node, transform( node ) );
+
+        expectOrder( "gid:aid::jar", "gid2:aid::jar" );
+        expectCycle( true );
+    }
+
+    @Test
+    public void testCycles()
+        throws Exception
+    {
+        DependencyNode node = parseResource( "cycles.txt" );
+        assertSame( node, transform( node ) );
+
+        expectOrder( "*", "*", "*", "gid:aid::jar" );
+        expectCycle( true );
+    }
+
+    @Test
+    public void testNoConflicts()
+        throws Exception
+    {
+        DependencyNode node = parseResource( "no-conflicts.txt" );
+        assertSame( node, transform( node ) );
+
+        expectOrder( "gid:aid::jar", "gid3:aid::jar", "gid2:aid::jar", "gid4:aid::jar" );
+        expectCycle( false );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictMarkerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictMarkerTest.java
new file mode 100644
index 0000000..550a0c6
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictMarkerTest.java
@@ -0,0 +1,122 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Map;
+
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.util.graph.transformer.ConflictMarker;
+import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
+import org.junit.Test;
+
+/**
+ */
+public class ConflictMarkerTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    @Override
+    protected DependencyGraphTransformer newTransformer()
+    {
+        return new ConflictMarker();
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/conflict-marker/" );
+    }
+
+    @Test
+    public void testSimple()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "simple.txt" );
+
+        assertSame( root, transform( root ) );
+
+        Map<?, ?> ids = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        assertNotNull( ids );
+
+        assertNull( ids.get( root ) );
+        assertNotNull( ids.get( root.getChildren().get( 0 ) ) );
+        assertNotNull( ids.get( root.getChildren().get( 1 ) ) );
+        assertNotSame( ids.get( root.getChildren().get( 0 ) ), ids.get( root.getChildren().get( 1 ) ) );
+        assertFalse( ids.get( root.getChildren().get( 0 ) ).equals( ids.get( root.getChildren().get( 1 ) ) ) );
+    }
+
+    @Test
+    public void testRelocation1()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "relocation1.txt" );
+
+        assertSame( root, transform( root ) );
+
+        Map<?, ?> ids = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        assertNotNull( ids );
+
+        assertNull( ids.get( root ) );
+        assertNotNull( ids.get( root.getChildren().get( 0 ) ) );
+        assertNotNull( ids.get( root.getChildren().get( 1 ) ) );
+        assertSame( ids.get( root.getChildren().get( 0 ) ), ids.get( root.getChildren().get( 1 ) ) );
+    }
+
+    @Test
+    public void testRelocation2()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "relocation2.txt" );
+
+        assertSame( root, transform( root ) );
+
+        Map<?, ?> ids = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        assertNotNull( ids );
+
+        assertNull( ids.get( root ) );
+        assertNotNull( ids.get( root.getChildren().get( 0 ) ) );
+        assertNotNull( ids.get( root.getChildren().get( 1 ) ) );
+        assertSame( ids.get( root.getChildren().get( 0 ) ), ids.get( root.getChildren().get( 1 ) ) );
+    }
+
+    @Test
+    public void testRelocation3()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "relocation3.txt" );
+
+        assertSame( root, transform( root ) );
+
+        Map<?, ?> ids = (Map<?, ?>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        assertNotNull( ids );
+
+        assertNull( ids.get( root ) );
+        assertNotNull( ids.get( root.getChildren().get( 0 ) ) );
+        assertNotNull( ids.get( root.getChildren().get( 1 ) ) );
+        assertNotNull( ids.get( root.getChildren().get( 2 ) ) );
+        assertSame( ids.get( root.getChildren().get( 0 ) ), ids.get( root.getChildren().get( 1 ) ) );
+        assertSame( ids.get( root.getChildren().get( 1 ) ), ids.get( root.getChildren().get( 2 ) ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefinerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefinerTest.java
new file mode 100644
index 0000000..bb0d65a
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaDependencyContextRefinerTest.java
@@ -0,0 +1,117 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.eclipse.aether.util.graph.transformer.JavaDependencyContextRefiner;
+import org.junit.Test;
+
+/**
+ */
+public class JavaDependencyContextRefinerTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    @Override
+    protected DependencyGraphTransformer newTransformer()
+    {
+        return new JavaDependencyContextRefiner();
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/context-refiner/" );
+    }
+
+    @Test
+    public void testDoNotRefineOtherContext()
+        throws Exception
+    {
+        DependencyNode node = parseLiteral( "gid:aid:cls:ver" );
+        node.setRequestContext( "otherContext" );
+
+        DependencyNode refinedNode = transform( node );
+        assertEquals( node, refinedNode );
+    }
+
+    @Test
+    public void testRefineToCompile()
+        throws Exception
+    {
+        String expected = "project/compile";
+
+        DependencyNode node = parseLiteral( "gid:aid:ver compile" );
+        node.setRequestContext( "project" );
+        DependencyNode refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+
+        node = parseLiteral( "gid:aid:ver system" );
+        node.setRequestContext( "project" );
+        refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+
+        node = parseLiteral( "gid:aid:ver provided" );
+        node.setRequestContext( "project" );
+        refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+    }
+
+    @Test
+    public void testRefineToTest()
+        throws Exception
+    {
+        String expected = "project/test";
+
+        DependencyNode node = parseLiteral( "gid:aid:ver test" );
+        node.setRequestContext( "project" );
+        DependencyNode refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+    }
+
+    @Test
+    public void testRefineToRuntime()
+        throws Exception
+    {
+        String expected = "project/runtime";
+
+        DependencyNode node = parseLiteral( "gid:aid:ver runtime" );
+        node.setRequestContext( "project" );
+        DependencyNode refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+    }
+
+    @Test
+    public void testDoNotRefineUnknownScopes()
+        throws Exception
+    {
+        String expected = "project";
+
+        DependencyNode node = parseLiteral( "gid:aid:ver unknownScope" );
+        node.setRequestContext( "project" );
+        DependencyNode refinedNode = transform( node );
+        assertEquals( expected, refinedNode.getRequestContext() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelectorTest.java
new file mode 100644
index 0000000..09f9c33
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/JavaScopeSelectorTest.java
@@ -0,0 +1,261 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Locale;
+
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class JavaScopeSelectorTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    private enum Scope
+    {
+        TEST, PROVIDED, RUNTIME, COMPILE;
+
+        @Override
+        public String toString()
+        {
+            return super.name().toLowerCase( Locale.ENGLISH );
+        }
+    }
+
+    @Override
+    protected DependencyGraphTransformer newTransformer()
+    {
+        return new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(),
+                                     new SimpleOptionalitySelector(), new JavaScopeDeriver() );
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/scope-calculator/" );
+    }
+
+    private void expectScope( String expected, DependencyNode root, int... coords )
+    {
+        expectScope( null, expected, root, coords );
+    }
+
+    private void expectScope( String msg, String expected, DependencyNode root, int... coords )
+    {
+        if ( msg == null )
+        {
+            msg = "";
+        }
+        try
+        {
+            DependencyNode node = root;
+            node = path( node, coords );
+
+            assertEquals( msg + "\nculprit: " + node.toString() + "\n", expected, node.getDependency().getScope() );
+        }
+        catch ( IndexOutOfBoundsException e )
+        {
+            throw new IllegalArgumentException( "illegal coordinates for child", e );
+        }
+        catch ( NullPointerException e )
+        {
+            throw new IllegalArgumentException( "illegal coordinates for child", e );
+        }
+    }
+
+    private DependencyNode path( DependencyNode node, int... coords )
+    {
+        for ( int coord : coords )
+        {
+            node = node.getChildren().get( coord );
+        }
+        return node;
+    }
+
+    @Test
+    public void testScopeInheritanceProvided()
+        throws Exception
+    {
+        String resource = "inheritance.txt";
+
+        String expected = "test";
+        DependencyNode root = transform( parseResource( resource, "provided", "test" ) );
+        expectScope( parser.dump( root ), expected, root, 0, 0 );
+    }
+
+    @Test
+    public void testConflictWinningScopeGetsUsedForInheritance()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "conflict-and-inheritance.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "compile", root, 0, 0 );
+        expectScope( "compile", root, 0, 0, 0 );
+    }
+
+    @Test
+    public void testScopeOfDirectDependencyWinsConflictAndGetsUsedForInheritanceToChildrenEverywhereInGraph()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "direct-with-conflict-and-inheritance.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "test", root, 0, 0 );
+    }
+
+    @Test
+    public void testCycleA()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "cycle-a.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "compile", root, 0 );
+        expectScope( "runtime", root, 1 );
+    }
+
+    @Test
+    public void testCycleB()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "cycle-b.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "runtime", root, 0 );
+        expectScope( "compile", root, 1 );
+    }
+
+    @Test
+    public void testCycleC()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "cycle-c.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "runtime", root, 0 );
+        expectScope( "runtime", root, 0, 0 );
+        expectScope( "runtime", root, 1 );
+        expectScope( "runtime", root, 1, 0 );
+    }
+
+    @Test
+    public void testCycleD()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "cycle-d.txt" );
+        assertSame( root, transform( root ) );
+
+        expectScope( "compile", root, 0 );
+        expectScope( "compile", root, 0, 0 );
+    }
+
+    @Test
+    public void testDirectNodesAlwaysWin()
+        throws Exception
+    {
+
+        for ( Scope directScope : Scope.values() )
+        {
+            String direct = directScope.toString();
+
+            DependencyNode root = parseResource( "direct-nodes-winning.txt", direct );
+
+            String msg =
+                String.format( "direct node should be setting scope ('%s') for all nodes.\n" + parser.dump( root ),
+                               direct );
+            assertSame( root, transform( root ) );
+            msg += "\ntransformed:\n" + parser.dump( root );
+
+            expectScope( msg, direct, root, 0 );
+        }
+    }
+
+    @Test
+    public void testNonDirectMultipleInheritance()
+        throws Exception
+    {
+        for ( Scope scope1 : Scope.values() )
+        {
+            for ( Scope scope2 : Scope.values() )
+            {
+                DependencyNode root = parseResource( "multiple-inheritance.txt", scope1.toString(), scope2.toString() );
+
+                String expected = scope1.compareTo( scope2 ) >= 0 ? scope1.toString() : scope2.toString();
+                String msg = String.format( "expected '%s' to win\n" + parser.dump( root ), expected );
+
+                assertSame( root, transform( root ) );
+                msg += "\ntransformed:\n" + parser.dump( root );
+
+                expectScope( msg, expected, root, 0, 0 );
+            }
+        }
+    }
+
+    @Test
+    public void testConflictScopeOrdering()
+        throws Exception
+    {
+        for ( Scope scope1 : Scope.values() )
+        {
+            for ( Scope scope2 : Scope.values() )
+            {
+                DependencyNode root = parseResource( "dueling-scopes.txt", scope1.toString(), scope2.toString() );
+
+                String expected = scope1.compareTo( scope2 ) >= 0 ? scope1.toString() : scope2.toString();
+                String msg = String.format( "expected '%s' to win\n" + parser.dump( root ), expected );
+
+                assertSame( root, transform( root ) );
+                msg += "\ntransformed:\n" + parser.dump( root );
+
+                expectScope( msg, expected, root, 0, 0 );
+            }
+        }
+    }
+
+    /**
+     * obscure case (illegal maven POM).
+     */
+    @Test
+    public void testConflictingDirectNodes()
+        throws Exception
+    {
+        for ( Scope scope1 : Scope.values() )
+        {
+            for ( Scope scope2 : Scope.values() )
+            {
+                DependencyNode root = parseResource( "conflicting-direct-nodes.txt", scope1.toString(), scope2.toString() );
+
+                String expected = scope1.toString();
+                String msg = String.format( "expected '%s' to win\n" + parser.dump( root ), expected );
+
+                assertSame( root, transform( root ) );
+                msg += "\ntransformed:\n" + parser.dump( root );
+
+                expectScope( msg, expected, root, 0 );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelectorTest.java
new file mode 100644
index 0000000..b71adab
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelectorTest.java
@@ -0,0 +1,244 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.eclipse.aether.collection.UnsolvableVersionConflictException;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+/**
+ */
+public class NearestVersionSelectorTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    @Override
+    protected ConflictResolver newTransformer()
+    {
+        return new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(),
+                                     new SimpleOptionalitySelector(), new JavaScopeDeriver() );
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/version-resolver/" );
+    }
+
+    @Test
+    public void testSelectHighestVersionFromMultipleVersionsAtSameLevel()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "sibling-versions.txt" );
+        assertSame( root, transform( root ) );
+
+        assertEquals( 1, root.getChildren().size() );
+        assertEquals( "3", root.getChildren().get( 0 ).getArtifact().getVersion() );
+    }
+
+    @Test
+    public void testSelectedVersionAtDeeperLevelThanOriginallySeen()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "nearest-underneath-loser-a.txt" );
+
+        assertSame( root, transform( root ) );
+
+        List<DependencyNode> trail = find( root, "j" );
+        assertEquals( 5, trail.size() );
+    }
+
+    @Test
+    public void testNearestDirtyVersionUnderneathRemovedNode()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "nearest-underneath-loser-b.txt" );
+
+        assertSame( root, transform( root ) );
+
+        List<DependencyNode> trail = find( root, "j" );
+        assertEquals( 5, trail.size() );
+    }
+
+    @Test
+    public void testViolationOfHardConstraintFallsBackToNearestSeenNotFirstSeen()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "range-backtracking.txt" );
+
+        assertSame( root, transform( root ) );
+
+        List<DependencyNode> trail = find( root, "x" );
+        assertEquals( 3, trail.size() );
+        assertEquals( "2", trail.get( 0 ).getArtifact().getVersion() );
+    }
+
+    @Test
+    public void testCyclicConflictIdGraph()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "conflict-id-cycle.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( "a", root.getChildren().get( 0 ).getArtifact().getArtifactId() );
+        assertEquals( "b", root.getChildren().get( 1 ).getArtifact().getArtifactId() );
+        assertTrue( root.getChildren().get( 0 ).getChildren().isEmpty() );
+        assertTrue( root.getChildren().get( 1 ).getChildren().isEmpty() );
+    }
+
+    @Test( expected = UnsolvableVersionConflictException.class )
+    public void testUnsolvableRangeConflictBetweenHardConstraints()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "unsolvable.txt" );
+
+        assertSame( root, transform( root ) );
+    }
+
+    @Test( expected = UnsolvableVersionConflictException.class )
+    public void testUnsolvableRangeConflictWithUnrelatedCycle()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "unsolvable-with-cycle.txt" );
+
+        transform( root );
+    }
+
+    @Test
+    public void testSolvableConflictBetweenHardConstraints()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "ranges.txt" );
+
+        assertSame( root, transform( root ) );
+    }
+
+    @Test
+    public void testConflictGroupCompletelyDroppedFromResolvedTree()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "dead-conflict-group.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( "a", root.getChildren().get( 0 ).getArtifact().getArtifactId() );
+        assertEquals( "b", root.getChildren().get( 1 ).getArtifact().getArtifactId() );
+        assertTrue( root.getChildren().get( 0 ).getChildren().isEmpty() );
+        assertTrue( root.getChildren().get( 1 ).getChildren().isEmpty() );
+    }
+
+    @Test
+    public void testNearestSoftVersionPrunedByFartherRange()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "soft-vs-range.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( "a", root.getChildren().get( 0 ).getArtifact().getArtifactId() );
+        assertEquals( 0, root.getChildren().get( 0 ).getChildren().size() );
+        assertEquals( "b", root.getChildren().get( 1 ).getArtifact().getArtifactId() );
+        assertEquals( 1, root.getChildren().get( 1 ).getChildren().size() );
+    }
+
+    @Test
+    public void testCyclicGraph()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "cycle.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( 1, root.getChildren().get( 0 ).getChildren().size() );
+        assertEquals( 0, root.getChildren().get( 0 ).getChildren().get( 0 ).getChildren().size() );
+        assertEquals( 0, root.getChildren().get( 1 ).getChildren().size() );
+    }
+
+    @Test
+    public void testLoop()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "loop.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 0, root.getChildren().size() );
+    }
+
+    @Test
+    public void testOverlappingCycles()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "overlapping-cycles.txt" );
+
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+    }
+
+    @Test
+    public void testScopeDerivationAndConflictResolutionCantHappenForAllNodesBeforeVersionSelection()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "scope-vs-version.txt" );
+
+        assertSame( root, transform( root ) );
+
+        DependencyNode[] nodes = find( root, "y" ).toArray( new DependencyNode[0] );
+        assertEquals( 3, nodes.length );
+        assertEquals( "test", nodes[1].getDependency().getScope() );
+        assertEquals( "test", nodes[0].getDependency().getScope() );
+    }
+
+    @Test
+    public void testVerboseMode()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "verbose.txt" );
+
+        session.setConfigProperty( ConflictResolver.CONFIG_PROP_VERBOSE, Boolean.TRUE );
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( 1, root.getChildren().get( 0 ).getChildren().size() );
+        DependencyNode winner = root.getChildren().get( 0 ).getChildren().get( 0 );
+        assertEquals( "test", winner.getDependency().getScope() );
+        assertEquals( "compile", winner.getData().get( ConflictResolver.NODE_DATA_ORIGINAL_SCOPE ) );
+        assertEquals( false, winner.getData().get( ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY) );
+        assertEquals( 1, root.getChildren().get( 1 ).getChildren().size() );
+        DependencyNode loser = root.getChildren().get( 1 ).getChildren().get( 0 );
+        assertEquals( "test", loser.getDependency().getScope() );
+        assertEquals( 0, loser.getChildren().size() );
+        assertSame( winner, loser.getData().get( ConflictResolver.NODE_DATA_WINNER ) );
+        assertEquals( "compile", loser.getData().get( ConflictResolver.NODE_DATA_ORIGINAL_SCOPE ) );
+        assertEquals( false, loser.getData().get( ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/RootQueueTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/RootQueueTest.java
new file mode 100644
index 0000000..f609ed7
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/RootQueueTest.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.util.graph.transformer.ConflictIdSorter.ConflictId;
+import org.eclipse.aether.util.graph.transformer.ConflictIdSorter.RootQueue;
+import org.junit.Test;
+
+public class RootQueueTest
+{
+
+    @Test
+    public void testIsEmpty()
+    {
+        ConflictId id = new ConflictId( "a", 0 );
+        RootQueue queue = new RootQueue( 10 );
+        assertTrue( queue.isEmpty() );
+        queue.add( id );
+        assertFalse( queue.isEmpty() );
+        assertSame( id, queue.remove() );
+        assertTrue( queue.isEmpty() );
+    }
+
+    @Test
+    public void testAddSortsByDepth()
+    {
+        ConflictId id1 = new ConflictId( "a", 0 );
+        ConflictId id2 = new ConflictId( "b", 1 );
+        ConflictId id3 = new ConflictId( "c", 2 );
+        ConflictId id4 = new ConflictId( "d", 3 );
+
+        RootQueue queue = new RootQueue( 10 );
+        queue.add( id1 );
+        queue.add( id2 );
+        queue.add( id3 );
+        queue.add( id4 );
+        assertSame( id1, queue.remove() );
+        assertSame( id2, queue.remove() );
+        assertSame( id3, queue.remove() );
+        assertSame( id4, queue.remove() );
+
+        queue = new RootQueue( 10 );
+        queue.add( id4 );
+        queue.add( id3 );
+        queue.add( id2 );
+        queue.add( id1 );
+        assertSame( id1, queue.remove() );
+        assertSame( id2, queue.remove() );
+        assertSame( id3, queue.remove() );
+        assertSame( id4, queue.remove() );
+    }
+
+    @Test
+    public void testAddWithArrayCompact()
+    {
+        ConflictId id = new ConflictId( "a", 0 );
+
+        RootQueue queue = new RootQueue( 10 );
+        assertTrue( queue.isEmpty() );
+        queue.add( id );
+        assertFalse( queue.isEmpty() );
+        assertSame( id, queue.remove() );
+        assertTrue( queue.isEmpty() );
+        queue.add( id );
+        assertFalse( queue.isEmpty() );
+        assertSame( id, queue.remove() );
+        assertTrue( queue.isEmpty() );
+    }
+
+    @Test
+    public void testAddMinimumAfterSomeRemoves()
+    {
+        ConflictId id1 = new ConflictId( "a", 0 );
+        ConflictId id2 = new ConflictId( "b", 1 );
+        ConflictId id3 = new ConflictId( "c", 2 );
+
+        RootQueue queue = new RootQueue( 10 );
+        queue.add( id2 );
+        queue.add( id3 );
+        assertSame( id2, queue.remove() );
+        queue.add( id1 );
+        assertSame( id1, queue.remove() );
+        assertSame( id3, queue.remove() );
+        assertTrue( queue.isEmpty() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleConflictMarker.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleConflictMarker.java
new file mode 100644
index 0000000..df30368
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleConflictMarker.java
@@ -0,0 +1,80 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
+
+/**
+ * Set "groupId:artId:classifier:extension" as conflict marker for every node.
+ */
+class SimpleConflictMarker
+    implements DependencyGraphTransformer
+{
+
+    public DependencyNode transformGraph( DependencyNode node, DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        @SuppressWarnings( "unchecked" )
+        Map<DependencyNode, Object> conflictIds =
+            (Map<DependencyNode, Object>) context.get( TransformationContextKeys.CONFLICT_IDS );
+        if ( conflictIds == null )
+        {
+            conflictIds = new IdentityHashMap<DependencyNode, Object>();
+            context.put( TransformationContextKeys.CONFLICT_IDS, conflictIds );
+        }
+
+        mark( node, conflictIds );
+
+        return node;
+    }
+
+    private void mark( DependencyNode node, Map<DependencyNode, Object> conflictIds )
+    {
+        Dependency dependency = node.getDependency();
+        if ( dependency != null )
+        {
+            Artifact artifact = dependency.getArtifact();
+
+            String key =
+                String.format( "%s:%s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(),
+                               artifact.getClassifier(), artifact.getExtension() );
+
+            if ( conflictIds.put( node, key ) != null )
+            {
+                return;
+            }
+        }
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            mark( child, conflictIds );
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelectorTest.java
new file mode 100644
index 0000000..f6d268f
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/SimpleOptionalitySelectorTest.java
@@ -0,0 +1,83 @@
+package org.eclipse.aether.util.graph.transformer;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class SimpleOptionalitySelectorTest
+    extends AbstractDependencyGraphTransformerTest
+{
+
+    @Override
+    protected DependencyGraphTransformer newTransformer()
+    {
+        return new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(),
+                                     new SimpleOptionalitySelector(), new JavaScopeDeriver() );
+    }
+
+    @Override
+    protected DependencyGraphParser newParser()
+    {
+        return new DependencyGraphParser( "transformer/optionality-selector/" );
+    }
+
+    @Test
+    public void testDeriveOptionality()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "derive.txt" );
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( true, root.getChildren().get( 0 ).getDependency().isOptional() );
+        assertEquals( true, root.getChildren().get( 0 ).getChildren().get( 0 ).getDependency().isOptional() );
+        assertEquals( false, root.getChildren().get( 1 ).getDependency().isOptional() );
+        assertEquals( false, root.getChildren().get( 1 ).getChildren().get( 0 ).getDependency().isOptional() );
+    }
+
+    @Test
+    public void testResolveOptionalityConflict_NonOptionalWins()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "conflict.txt" );
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( true, root.getChildren().get( 0 ).getDependency().isOptional() );
+        assertEquals( false, root.getChildren().get( 0 ).getChildren().get( 0 ).getDependency().isOptional() );
+    }
+
+    @Test
+    public void testResolveOptionalityConflict_DirectDeclarationWins()
+        throws Exception
+    {
+        DependencyNode root = parseResource( "conflict-direct-dep.txt" );
+        assertSame( root, transform( root ) );
+
+        assertEquals( 2, root.getChildren().size() );
+        assertEquals( true, root.getChildren().get( 1 ).getDependency().isOptional() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverserTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverserTest.java
new file mode 100644
index 0000000..74d744e
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/AndDependencyTraverserTest.java
@@ -0,0 +1,154 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+import org.junit.Test;
+
+public class AndDependencyTraverserTest
+{
+
+    static class DummyDependencyTraverser
+        implements DependencyTraverser
+    {
+
+        private final boolean traverse;
+
+        private final DependencyTraverser child;
+
+        public DummyDependencyTraverser()
+        {
+            this( true );
+        }
+
+        public DummyDependencyTraverser( boolean traverse )
+        {
+            this.traverse = traverse;
+            this.child = this;
+        }
+
+        public DummyDependencyTraverser( boolean traverse, DependencyTraverser child )
+        {
+            this.traverse = traverse;
+            this.child = child;
+        }
+
+        public boolean traverseDependency( Dependency dependency )
+        {
+            return traverse;
+        }
+
+        public DependencyTraverser deriveChildTraverser( DependencyCollectionContext context )
+        {
+            return child;
+        }
+
+    }
+
+    @Test
+    public void testNewInstance()
+    {
+        assertNull( AndDependencyTraverser.newInstance( null, null ) );
+        DependencyTraverser traverser = new DummyDependencyTraverser();
+        assertSame( traverser, AndDependencyTraverser.newInstance( traverser, null ) );
+        assertSame( traverser, AndDependencyTraverser.newInstance( null, traverser ) );
+        assertSame( traverser, AndDependencyTraverser.newInstance( traverser, traverser ) );
+        assertNotNull( AndDependencyTraverser.newInstance( traverser, new DummyDependencyTraverser() ) );
+    }
+
+    @Test
+    public void testTraverseDependency()
+    {
+        Dependency dependency = new Dependency( new DefaultArtifact( "g:a:v:1" ), "runtime" );
+
+        DependencyTraverser traverser = new AndDependencyTraverser();
+        assertTrue( traverser.traverseDependency( dependency ) );
+
+        traverser =
+            new AndDependencyTraverser( new DummyDependencyTraverser( false ), new DummyDependencyTraverser( false ) );
+        assertFalse( traverser.traverseDependency( dependency ) );
+
+        traverser =
+            new AndDependencyTraverser( new DummyDependencyTraverser( true ), new DummyDependencyTraverser( false ) );
+        assertFalse( traverser.traverseDependency( dependency ) );
+
+        traverser =
+            new AndDependencyTraverser( new DummyDependencyTraverser( true ), new DummyDependencyTraverser( true ) );
+        assertTrue( traverser.traverseDependency( dependency ) );
+    }
+
+    @Test
+    public void testDeriveChildTraverser_Unchanged()
+    {
+        DependencyTraverser other1 = new DummyDependencyTraverser( true );
+        DependencyTraverser other2 = new DummyDependencyTraverser( false );
+        DependencyTraverser traverser = new AndDependencyTraverser( other1, other2 );
+        assertSame( traverser, traverser.deriveChildTraverser( null ) );
+    }
+
+    @Test
+    public void testDeriveChildTraverser_OneRemaining()
+    {
+        DependencyTraverser other1 = new DummyDependencyTraverser( true );
+        DependencyTraverser other2 = new DummyDependencyTraverser( false, null );
+        DependencyTraverser traverser = new AndDependencyTraverser( other1, other2 );
+        assertSame( other1, traverser.deriveChildTraverser( null ) );
+    }
+
+    @Test
+    public void testDeriveChildTraverser_ZeroRemaining()
+    {
+        DependencyTraverser other1 = new DummyDependencyTraverser( true, null );
+        DependencyTraverser other2 = new DummyDependencyTraverser( false, null );
+        DependencyTraverser traverser = new AndDependencyTraverser( other1, other2 );
+        assertNull( traverser.deriveChildTraverser( null ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        DependencyTraverser other1 = new DummyDependencyTraverser( true );
+        DependencyTraverser other2 = new DummyDependencyTraverser( false );
+        DependencyTraverser traverser1 = new AndDependencyTraverser( other1, other2 );
+        DependencyTraverser traverser2 = new AndDependencyTraverser( other2, other1 );
+        DependencyTraverser traverser3 = new AndDependencyTraverser( other1 );
+        assertEquals( traverser1, traverser1 );
+        assertEquals( traverser1, traverser2 );
+        assertNotEquals( traverser1, traverser3 );
+        assertNotEquals( traverser1, this );
+        assertNotEquals( traverser1, null );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        DependencyTraverser other1 = new DummyDependencyTraverser( true );
+        DependencyTraverser other2 = new DummyDependencyTraverser( false );
+        DependencyTraverser traverser1 = new AndDependencyTraverser( other1, other2 );
+        DependencyTraverser traverser2 = new AndDependencyTraverser( other2, other1 );
+        assertEquals( traverser1.hashCode(), traverser2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverserTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverserTest.java
new file mode 100644
index 0000000..641e593
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/FatArtifactTraverserTest.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+import org.junit.Test;
+
+public class FatArtifactTraverserTest
+{
+
+    @Test
+    public void testTraverseDependency()
+    {
+        DependencyTraverser traverser = new FatArtifactTraverser();
+        Map<String, String> props = null;
+        assertTrue( traverser.traverseDependency( new Dependency( new DefaultArtifact( "g:a:v:1", props ), "test" ) ) );
+        props = Collections.singletonMap( ArtifactProperties.INCLUDES_DEPENDENCIES, "false" );
+        assertTrue( traverser.traverseDependency( new Dependency( new DefaultArtifact( "g:a:v:1", props ), "test" ) ) );
+        props = Collections.singletonMap( ArtifactProperties.INCLUDES_DEPENDENCIES, "unrecognized" );
+        assertTrue( traverser.traverseDependency( new Dependency( new DefaultArtifact( "g:a:v:1", props ), "test" ) ) );
+        props = Collections.singletonMap( ArtifactProperties.INCLUDES_DEPENDENCIES, "true" );
+        assertFalse( traverser.traverseDependency( new Dependency( new DefaultArtifact( "g:a:v:1", props ), "test" ) ) );
+    }
+
+    @Test
+    public void testDeriveChildTraverser()
+    {
+        DependencyTraverser traverser = new FatArtifactTraverser();
+        assertSame( traverser, traverser.deriveChildTraverser( null ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        DependencyTraverser traverser1 = new FatArtifactTraverser();
+        DependencyTraverser traverser2 = new FatArtifactTraverser();
+        assertEquals( traverser1, traverser1 );
+        assertEquals( traverser1, traverser2 );
+        assertNotEquals( traverser1, this );
+        assertNotEquals( traverser1, null );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        DependencyTraverser traverser1 = new FatArtifactTraverser();
+        DependencyTraverser traverser2 = new FatArtifactTraverser();
+        assertEquals( traverser1.hashCode(), traverser2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverserTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverserTest.java
new file mode 100644
index 0000000..0ac1d91
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/traverser/StaticDependencyTraverserTest.java
@@ -0,0 +1,70 @@
+package org.eclipse.aether.util.graph.traverser;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyTraverser;
+import org.eclipse.aether.graph.Dependency;
+import org.junit.Test;
+
+public class StaticDependencyTraverserTest
+{
+
+    @Test
+    public void testTraverseDependency()
+    {
+        Dependency dependency = new Dependency( new DefaultArtifact( "g:a:v:1" ), "runtime" );
+        DependencyTraverser traverser = new StaticDependencyTraverser( true );
+        assertTrue( traverser.traverseDependency( dependency ) );
+        traverser = new StaticDependencyTraverser( false );
+        assertFalse( traverser.traverseDependency( dependency ) );
+    }
+
+    @Test
+    public void testDeriveChildTraverser()
+    {
+        DependencyTraverser traverser = new StaticDependencyTraverser( true );
+        assertSame( traverser, traverser.deriveChildTraverser( null ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        DependencyTraverser traverser1 = new StaticDependencyTraverser( true );
+        DependencyTraverser traverser2 = new StaticDependencyTraverser( true );
+        DependencyTraverser traverser3 = new StaticDependencyTraverser( false );
+        assertEquals( traverser1, traverser1 );
+        assertEquals( traverser1, traverser2 );
+        assertNotEquals( traverser1, traverser3 );
+        assertNotEquals( traverser1, this );
+        assertNotEquals( traverser1, null );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        DependencyTraverser traverser1 = new StaticDependencyTraverser( true );
+        DependencyTraverser traverser2 = new StaticDependencyTraverser( true );
+        assertEquals( traverser1.hashCode(), traverser2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/AbstractVersionFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/AbstractVersionFilterTest.java
new file mode 100644
index 0000000..13fd4b0
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/AbstractVersionFilterTest.java
@@ -0,0 +1,96 @@
+package org.eclipse.aether.util.graph.versions;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Iterator;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionScheme;
+import org.junit.After;
+import org.junit.Before;
+
+public abstract class AbstractVersionFilterTest
+{
+
+    protected DefaultRepositorySystemSession session;
+
+    @Before
+    public void setUp()
+    {
+        session = TestUtils.newSession();
+    }
+
+    @After
+    public void tearDown()
+    {
+        session = null;
+    }
+
+    protected VersionFilter.VersionFilterContext newContext( String gav, String... versions )
+    {
+        VersionRangeRequest request = new VersionRangeRequest();
+        request.setArtifact( new DefaultArtifact( gav ) );
+        VersionRangeResult result = new VersionRangeResult( request );
+        VersionScheme scheme = new GenericVersionScheme();
+        try
+        {
+            result.setVersionConstraint( scheme.parseVersionConstraint( request.getArtifact().getVersion() ) );
+            for ( String version : versions )
+            {
+                result.addVersion( scheme.parseVersion( version ) );
+            }
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            throw new IllegalArgumentException( e );
+        }
+        return TestUtils.newVersionFilterContext( session, result );
+    }
+
+    protected VersionFilter derive( VersionFilter filter, String gav )
+    {
+        return filter.deriveChildFilter( TestUtils.newCollectionContext( session,
+                                                                         new Dependency( new DefaultArtifact( gav ), "" ),
+                                                                         null ) );
+    }
+
+    protected void assertVersions( VersionFilter.VersionFilterContext context, String... versions )
+    {
+        assertEquals( versions.length, context.getCount() );
+        Iterator<Version> it = context.iterator();
+        for ( String version : versions )
+        {
+            assertTrue( it.hasNext() );
+            assertEquals( version, it.next().toString() );
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ChainedVersionFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ChainedVersionFilterTest.java
new file mode 100644
index 0000000..1e8a5bd
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ChainedVersionFilterTest.java
@@ -0,0 +1,85 @@
+package org.eclipse.aether.util.graph.versions;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.collection.VersionFilter.VersionFilterContext;
+import org.eclipse.aether.util.graph.version.ChainedVersionFilter;
+import org.eclipse.aether.util.graph.version.HighestVersionFilter;
+import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
+import org.junit.Test;
+
+public class ChainedVersionFilterTest
+    extends AbstractVersionFilterTest
+{
+
+    @Test
+    public void testFilterVersions()
+        throws Exception
+    {
+        VersionFilter filter =
+            ChainedVersionFilter.newInstance( new SnapshotVersionFilter(), new HighestVersionFilter() );
+        VersionFilterContext ctx = newContext( "g:a:[1,9]", "1", "2", "3-SNAPSHOT" );
+        filter.filterVersions( ctx );
+        assertVersions( ctx, "2" );
+    }
+
+    @Test
+    public void testDeriveChildFilter()
+    {
+        VersionFilter filter1 = new HighestVersionFilter();
+        VersionFilter filter2 = new VersionFilter()
+        {
+            public void filterVersions( VersionFilterContext context )
+            {
+            }
+
+            public VersionFilter deriveChildFilter( DependencyCollectionContext context )
+            {
+                return null;
+            }
+        };
+
+        VersionFilter filter = ChainedVersionFilter.newInstance( filter1 );
+        assertSame( filter, derive( filter, "g:a:1" ) );
+
+        filter = ChainedVersionFilter.newInstance( filter2 );
+        assertSame( null, derive( filter, "g:a:1" ) );
+
+        filter = ChainedVersionFilter.newInstance( filter1, filter2 );
+        assertSame( filter1, derive( filter, "g:a:1" ) );
+
+        filter = ChainedVersionFilter.newInstance( filter2, filter1 );
+        assertSame( filter1, derive( filter, "g:a:1" ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        VersionFilter filter = ChainedVersionFilter.newInstance( new HighestVersionFilter() );
+        assertFalse( filter.equals( null ) );
+        assertTrue( filter.equals( filter ) );
+        assertTrue( filter.equals( ChainedVersionFilter.newInstance( new HighestVersionFilter() ) ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ContextualSnapshotVersionFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ContextualSnapshotVersionFilterTest.java
new file mode 100644
index 0000000..dd88a66
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/ContextualSnapshotVersionFilterTest.java
@@ -0,0 +1,72 @@
+package org.eclipse.aether.util.graph.versions;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.collection.VersionFilter.VersionFilterContext;
+import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter;
+import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
+import org.junit.Test;
+
+public class ContextualSnapshotVersionFilterTest
+    extends AbstractVersionFilterTest
+{
+
+    @Test
+    public void testFilterVersions()
+        throws Exception
+    {
+        VersionFilter filter = new ContextualSnapshotVersionFilter();
+        VersionFilterContext ctx = newContext( "g:a:[1,9]", "1", "2-SNAPSHOT" );
+        filter.filterVersions( ctx );
+        assertVersions( ctx, "1", "2-SNAPSHOT" );
+
+        ctx = newContext( "g:a:[1,9]", "1", "2-SNAPSHOT" );
+        derive( filter, "g:a:1" ).filterVersions( ctx );
+        assertVersions( ctx, "1" );
+
+        ctx = newContext( "g:a:[1,9]", "1", "2-SNAPSHOT" );
+        session.setConfigProperty( ContextualSnapshotVersionFilter.CONFIG_PROP_ENABLE, "true" );
+        derive( filter, "g:a:1-SNAPSHOT" ).filterVersions( ctx );
+        assertVersions( ctx, "1" );
+    }
+
+    @Test
+    public void testDeriveChildFilter()
+    {
+        ContextualSnapshotVersionFilter filter = new ContextualSnapshotVersionFilter();
+        assertTrue( derive( filter, "g:a:1" ) instanceof SnapshotVersionFilter );
+        assertSame( null, derive( filter, "g:a:1-SNAPSHOT" ) );
+        session.setConfigProperty( ContextualSnapshotVersionFilter.CONFIG_PROP_ENABLE, "true" );
+        assertTrue( derive( filter, "g:a:1-SNAPSHOT" ) instanceof SnapshotVersionFilter );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        ContextualSnapshotVersionFilter filter = new ContextualSnapshotVersionFilter();
+        assertFalse( filter.equals( null ) );
+        assertTrue( filter.equals( filter ) );
+        assertTrue( filter.equals( new ContextualSnapshotVersionFilter() ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/HighestVersionFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/HighestVersionFilterTest.java
new file mode 100644
index 0000000..3926c66
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/HighestVersionFilterTest.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.util.graph.versions;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.VersionFilter.VersionFilterContext;
+import org.eclipse.aether.util.graph.version.HighestVersionFilter;
+import org.junit.Test;
+
+public class HighestVersionFilterTest
+    extends AbstractVersionFilterTest
+{
+
+    @Test
+    public void testFilterVersions()
+    {
+        HighestVersionFilter filter = new HighestVersionFilter();
+        VersionFilterContext ctx = newContext( "g:a:[1,9]", "1", "2", "3", "4", "5", "6", "7", "8", "9" );
+        filter.filterVersions( ctx );
+        assertVersions( ctx, "9" );
+    }
+
+    @Test
+    public void testDeriveChildFilter()
+    {
+        HighestVersionFilter filter = new HighestVersionFilter();
+        assertSame( filter, derive( filter, "g:a:1" ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        HighestVersionFilter filter = new HighestVersionFilter();
+        assertFalse( filter.equals( null ) );
+        assertTrue( filter.equals( filter ) );
+        assertTrue( filter.equals( new HighestVersionFilter() ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/SnapshotVersionFilterTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/SnapshotVersionFilterTest.java
new file mode 100644
index 0000000..70c26f9
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/versions/SnapshotVersionFilterTest.java
@@ -0,0 +1,57 @@
+package org.eclipse.aether.util.graph.versions;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.collection.VersionFilter.VersionFilterContext;
+import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
+import org.junit.Test;
+
+public class SnapshotVersionFilterTest
+    extends AbstractVersionFilterTest
+{
+
+    @Test
+    public void testFilterVersions()
+    {
+        SnapshotVersionFilter filter = new SnapshotVersionFilter();
+        VersionFilterContext ctx = newContext( "g:a:[1,9]", "1", "2-SNAPSHOT", "3.1", "4.0-SNAPSHOT", "5.0.0" );
+        filter.filterVersions( ctx );
+        assertVersions( ctx, "1", "3.1", "5.0.0" );
+    }
+
+    @Test
+    public void testDeriveChildFilter()
+    {
+        SnapshotVersionFilter filter = new SnapshotVersionFilter();
+        assertSame( filter, derive( filter, "g:a:1" ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        SnapshotVersionFilter filter = new SnapshotVersionFilter();
+        assertFalse( filter.equals( null ) );
+        assertTrue( filter.equals( filter ) );
+        assertTrue( filter.equals( new SnapshotVersionFilter() ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitorTest.java
new file mode 100644
index 0000000..65a02a8
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitorTest.java
@@ -0,0 +1,66 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class FilteringDependencyVisitorTest
+{
+
+    private DependencyNode parse( String resource )
+        throws Exception
+    {
+        return new DependencyGraphParser( "visitor/filtering/" ).parseResource( resource );
+    }
+
+    @Test
+    public void testFilterCalledWithProperParentStack()
+        throws Exception
+    {
+        DependencyNode root = parse( "parents.txt" );
+
+        final StringBuilder buffer = new StringBuilder( 256 );
+        DependencyFilter filter = new DependencyFilter()
+        {
+            public boolean accept( DependencyNode node, List<DependencyNode> parents )
+            {
+                for ( DependencyNode parent : parents )
+                {
+                    buffer.append( parent.getDependency().getArtifact().getArtifactId() );
+                }
+                buffer.append( "," );
+                return false;
+            }
+        };
+
+        FilteringDependencyVisitor visitor = new FilteringDependencyVisitor( new PreorderNodeListGenerator(), filter );
+        root.accept( visitor );
+
+        assertEquals( ",a,ba,cba,a,ea,", buffer.toString() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitorTest.java
new file mode 100644
index 0000000..cd766a0
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitorTest.java
@@ -0,0 +1,147 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class PathRecordingDependencyVisitorTest
+{
+
+    private DependencyNode parse( String resource )
+        throws Exception
+    {
+        return new DependencyGraphParser( "visitor/path-recorder/" ).parseResource( resource );
+    }
+
+    private void assertPath( List<DependencyNode> actual, String... expected )
+    {
+        assertEquals( actual.toString(), expected.length, actual.size() );
+        for ( int i = 0; i < expected.length; i++ )
+        {
+            DependencyNode node = actual.get( i );
+            assertEquals( actual.toString(), expected[i], node.getDependency().getArtifact().getArtifactId() );
+        }
+    }
+
+    @Test
+    public void testGetPaths_RecordsMatchesBeneathUnmatchedParents()
+        throws Exception
+    {
+        DependencyNode root = parse( "simple.txt" );
+
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( new ArtifactMatcher() );
+        root.accept( visitor );
+
+        List<List<DependencyNode>> paths = visitor.getPaths();
+        assertEquals( paths.toString(), 2, paths.size() );
+        assertPath( paths.get( 0 ), "a", "b", "x" );
+        assertPath( paths.get( 1 ), "a", "x" );
+    }
+
+    @Test
+    public void testGetPaths_DoesNotRecordMatchesBeneathMatchedParents()
+        throws Exception
+    {
+        DependencyNode root = parse( "nested.txt" );
+
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( new ArtifactMatcher() );
+        root.accept( visitor );
+
+        List<List<DependencyNode>> paths = visitor.getPaths();
+        assertEquals( paths.toString(), 1, paths.size() );
+        assertPath( paths.get( 0 ), "x" );
+    }
+
+    @Test
+    public void testGetPaths_RecordsMatchesBeneathMatchedParentsIfRequested()
+        throws Exception
+    {
+        DependencyNode root = parse( "nested.txt" );
+
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( new ArtifactMatcher(), false );
+        root.accept( visitor );
+
+        List<List<DependencyNode>> paths = visitor.getPaths();
+        assertEquals( paths.toString(), 3, paths.size() );
+        assertPath( paths.get( 0 ), "x" );
+        assertPath( paths.get( 1 ), "x", "a", "y" );
+        assertPath( paths.get( 2 ), "x", "y" );
+    }
+
+    @Test
+    public void testFilterCalledWithProperParentStack()
+        throws Exception
+    {
+        DependencyNode root = parse( "parents.txt" );
+
+        final StringBuilder buffer = new StringBuilder( 256 );
+        DependencyFilter filter = new DependencyFilter()
+        {
+            public boolean accept( DependencyNode node, List<DependencyNode> parents )
+            {
+                for ( DependencyNode parent : parents )
+                {
+                    buffer.append( parent.getDependency().getArtifact().getArtifactId() );
+                }
+                buffer.append( "," );
+                return false;
+            }
+        };
+
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( filter );
+        root.accept( visitor );
+
+        assertEquals( ",a,ba,cba,a,ea,", buffer.toString() );
+    }
+
+    @Test
+    public void testGetPaths_HandlesCycles()
+        throws Exception
+    {
+        DependencyNode root = parse( "cycle.txt" );
+
+        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor( new ArtifactMatcher(), false );
+        root.accept( visitor );
+
+        List<List<DependencyNode>> paths = visitor.getPaths();
+        assertEquals( paths.toString(), 4, paths.size() );
+        assertPath( paths.get( 0 ), "a", "b", "x" );
+        assertPath( paths.get( 1 ), "a", "x" );
+        assertPath( paths.get( 2 ), "a", "x", "b", "x" );
+        assertPath( paths.get( 3 ), "a", "x", "x" );
+    }
+
+    private static class ArtifactMatcher
+        implements DependencyFilter
+    {
+        public boolean accept( DependencyNode node, List<DependencyNode> parents )
+        {
+            return node.getDependency() != null && node.getDependency().getArtifact().getGroupId().equals( "match" );
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGeneratorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGeneratorTest.java
new file mode 100644
index 0000000..8d6f525
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGeneratorTest.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class PostorderNodeListGeneratorTest
+{
+
+    private DependencyNode parse( String resource )
+        throws Exception
+    {
+        return new DependencyGraphParser( "visitor/ordered-list/" ).parseResource( resource );
+    }
+
+    private void assertSequence( List<DependencyNode> actual, String... expected )
+    {
+        assertEquals( actual.toString(), expected.length, actual.size() );
+        for ( int i = 0; i < expected.length; i++ )
+        {
+            DependencyNode node = actual.get( i );
+            assertEquals( actual.toString(), expected[i], node.getDependency().getArtifact().getArtifactId() );
+        }
+    }
+
+    @Test
+    public void testOrdering()
+        throws Exception
+    {
+        DependencyNode root = parse( "simple.txt" );
+
+        PostorderNodeListGenerator visitor = new PostorderNodeListGenerator();
+        root.accept( visitor );
+
+        assertSequence( visitor.getNodes(), "c", "b", "e", "d", "a" );
+    }
+
+    @Test
+    public void testDuplicateSuppression()
+        throws Exception
+    {
+        DependencyNode root = parse( "cycles.txt" );
+
+        PostorderNodeListGenerator visitor = new PostorderNodeListGenerator();
+        root.accept( visitor );
+
+        assertSequence( visitor.getNodes(), "c", "b", "e", "d", "a" );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGeneratorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGeneratorTest.java
new file mode 100644
index 0000000..200dd3b
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGeneratorTest.java
@@ -0,0 +1,73 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class PreorderNodeListGeneratorTest
+{
+
+    private DependencyNode parse( String resource )
+        throws Exception
+    {
+        return new DependencyGraphParser( "visitor/ordered-list/" ).parseResource( resource );
+    }
+
+    private void assertSequence( List<DependencyNode> actual, String... expected )
+    {
+        assertEquals( actual.toString(), expected.length, actual.size() );
+        for ( int i = 0; i < expected.length; i++ )
+        {
+            DependencyNode node = actual.get( i );
+            assertEquals( actual.toString(), expected[i], node.getDependency().getArtifact().getArtifactId() );
+        }
+    }
+
+    @Test
+    public void testOrdering()
+        throws Exception
+    {
+        DependencyNode root = parse( "simple.txt" );
+
+        PreorderNodeListGenerator visitor = new PreorderNodeListGenerator();
+        root.accept( visitor );
+
+        assertSequence( visitor.getNodes(), "a", "b", "c", "d", "e" );
+    }
+
+    @Test
+    public void testDuplicateSuppression()
+        throws Exception
+    {
+        DependencyNode root = parse( "cycles.txt" );
+
+        PreorderNodeListGenerator visitor = new PreorderNodeListGenerator();
+        root.accept( visitor );
+
+        assertSequence( visitor.getNodes(), "a", "b", "c", "d", "e" );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitorTest.java
new file mode 100644
index 0000000..36cb6ac
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitorTest.java
@@ -0,0 +1,71 @@
+package org.eclipse.aether.util.graph.visitor;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.graph.DependencyVisitor;
+import org.eclipse.aether.internal.test.util.DependencyGraphParser;
+import org.junit.Test;
+
+public class TreeDependencyVisitorTest
+{
+
+    private DependencyNode parse( String resource )
+        throws Exception
+    {
+        return new DependencyGraphParser( "visitor/tree/" ).parseResource( resource );
+    }
+
+    @Test
+    public void testDuplicateSuppression()
+        throws Exception
+    {
+        DependencyNode root = parse( "cycles.txt" );
+
+        RecordingVisitor rec = new RecordingVisitor();
+        TreeDependencyVisitor visitor = new TreeDependencyVisitor( rec );
+        root.accept( visitor );
+
+        assertEquals( ">a >b >c <c <b >d <d <a ", rec.buffer.toString() );
+    }
+
+    private static class RecordingVisitor
+        implements DependencyVisitor
+    {
+
+        StringBuilder buffer = new StringBuilder( 256 );
+
+        public boolean visitEnter( DependencyNode node )
+        {
+            buffer.append( '>' ).append( node.getDependency().getArtifact().getArtifactId() ).append( ' ' );
+            return true;
+        }
+
+        public boolean visitLeave( DependencyNode node )
+        {
+            buffer.append( '<' ).append( node.getDependency().getArtifact().getArtifactId() ).append( ' ' );
+            return true;
+        }
+
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedRepositoryListenerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedRepositoryListenerTest.java
new file mode 100644
index 0000000..6eaa25b
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedRepositoryListenerTest.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether.util.listener;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.util.listener.ChainedRepositoryListener;
+import org.junit.Test;
+
+/**
+ */
+public class ChainedRepositoryListenerTest
+{
+
+    @Test
+    public void testAllEventTypesHandled()
+        throws Exception
+    {
+        for ( Method method : RepositoryListener.class.getMethods() )
+        {
+            assertNotNull( ChainedRepositoryListener.class.getDeclaredMethod( method.getName(),
+                                                                              method.getParameterTypes() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedTransferListenerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedTransferListenerTest.java
new file mode 100644
index 0000000..7e7e969
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/listener/ChainedTransferListenerTest.java
@@ -0,0 +1,46 @@
+package org.eclipse.aether.util.listener;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.util.listener.ChainedTransferListener;
+import org.junit.Test;
+
+/**
+ */
+public class ChainedTransferListenerTest
+{
+
+    @Test
+    public void testAllEventTypesHandled()
+        throws Exception
+    {
+        for ( Method method : TransferListener.class.getMethods() )
+        {
+            assertNotNull( ChainedTransferListener.class.getDeclaredMethod( method.getName(),
+                                                                            method.getParameterTypes() ) );
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ComponentAuthenticationTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ComponentAuthenticationTest.java
new file mode 100644
index 0000000..25d53f2
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/ComponentAuthenticationTest.java
@@ -0,0 +1,106 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+public class ComponentAuthenticationTest
+{
+
+    private static class Component
+    {
+    }
+
+    private RepositorySystemSession newSession()
+    {
+        return new DefaultRepositorySystemSession();
+    }
+
+    private RemoteRepository newRepo( Authentication auth )
+    {
+        return new RemoteRepository.Builder( "test", "default", "http://localhost" ).setAuthentication( auth ).build();
+    }
+
+    private AuthenticationContext newContext( Authentication auth )
+    {
+        return AuthenticationContext.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    private String newDigest( Authentication auth )
+    {
+        return AuthenticationDigest.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    @Test
+    public void testFill()
+    {
+        Component comp = new Component();
+        Authentication auth = new ComponentAuthentication( "key", comp );
+        AuthenticationContext context = newContext( auth );
+        assertEquals( null, context.get( "another-key" ) );
+        assertSame( comp, context.get( "key", Component.class ) );
+    }
+
+    @Test
+    public void testDigest()
+    {
+        Authentication auth1 = new ComponentAuthentication( "key", new Component() );
+        Authentication auth2 = new ComponentAuthentication( "key", new Component() );
+        String digest1 = newDigest( auth1 );
+        String digest2 = newDigest( auth2 );
+        assertEquals( digest1, digest2 );
+
+        Authentication auth3 = new ComponentAuthentication( "key", new Object() );
+        String digest3 = newDigest( auth3 );
+        assertFalse( digest3.equals( digest1 ) );
+
+        Authentication auth4 = new ComponentAuthentication( "Key", new Component() );
+        String digest4 = newDigest( auth4 );
+        assertFalse( digest4.equals( digest1 ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        Authentication auth1 = new ComponentAuthentication( "key", new Component() );
+        Authentication auth2 = new ComponentAuthentication( "key", new Component() );
+        Authentication auth3 = new ComponentAuthentication( "key", new Object() );
+        assertEquals( auth1, auth2 );
+        assertFalse( auth1.equals( auth3 ) );
+        assertFalse( auth1.equals( null ) );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        Authentication auth1 = new ComponentAuthentication( "key", new Component() );
+        Authentication auth2 = new ComponentAuthentication( "key", new Component() );
+        assertEquals( auth1.hashCode(), auth2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/DefaultProxySelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/DefaultProxySelectorTest.java
new file mode 100644
index 0000000..3eacbd5
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/DefaultProxySelectorTest.java
@@ -0,0 +1,76 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.util.repository.DefaultProxySelector;
+import org.junit.Test;
+
+/**
+ */
+public class DefaultProxySelectorTest
+{
+
+    private boolean isNonProxyHost( String host, String nonProxyHosts )
+    {
+        return new DefaultProxySelector.NonProxyHosts( nonProxyHosts ).isNonProxyHost( host );
+    }
+
+    @Test
+    public void testIsNonProxyHost_Blank()
+    {
+        assertFalse( isNonProxyHost( "www.eclipse.org", null ) );
+        assertFalse( isNonProxyHost( "www.eclipse.org", "" ) );
+    }
+
+    @Test
+    public void testIsNonProxyHost_Wildcard()
+    {
+        assertTrue( isNonProxyHost( "www.eclipse.org", "*" ) );
+        assertTrue( isNonProxyHost( "www.eclipse.org", "*.org" ) );
+        assertFalse( isNonProxyHost( "www.eclipse.org", "*.com" ) );
+        assertTrue( isNonProxyHost( "www.eclipse.org", "www.*" ) );
+        assertTrue( isNonProxyHost( "www.eclipse.org", "www.*.org" ) );
+    }
+
+    @Test
+    public void testIsNonProxyHost_Multiple()
+    {
+        assertTrue( isNonProxyHost( "eclipse.org", "eclipse.org|host2" ) );
+        assertTrue( isNonProxyHost( "eclipse.org", "host1|eclipse.org" ) );
+        assertTrue( isNonProxyHost( "eclipse.org", "host1|eclipse.org|host2" ) );
+    }
+
+    @Test
+    public void testIsNonProxyHost_Misc()
+    {
+        assertFalse( isNonProxyHost( "www.eclipse.org", "www.eclipse.com" ) );
+        assertFalse( isNonProxyHost( "www.eclipse.org", "eclipse.org" ) );
+    }
+
+    @Test
+    public void testIsNonProxyHost_CaseInsensitivity()
+    {
+        assertTrue( isNonProxyHost( "www.eclipse.org", "www.ECLIPSE.org" ) );
+        assertTrue( isNonProxyHost( "www.ECLIPSE.org", "www.eclipse.org" ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/JreProxySelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/JreProxySelectorTest.java
new file mode 100644
index 0000000..8eac55b
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/JreProxySelectorTest.java
@@ -0,0 +1,184 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class JreProxySelectorTest
+{
+
+    private abstract class AbstractProxySelector
+        extends java.net.ProxySelector
+    {
+        @Override
+        public void connectFailed( URI uri, SocketAddress sa, IOException ioe )
+        {
+        }
+    }
+
+    private ProxySelector selector = new JreProxySelector();
+
+    private java.net.ProxySelector original;
+
+    @Before
+    public void init()
+    {
+        original = java.net.ProxySelector.getDefault();
+    }
+
+    @After
+    public void exit()
+    {
+        java.net.ProxySelector.setDefault( original );
+        Authenticator.setDefault( null );
+    }
+
+    @Test
+    public void testGetProxy_InvalidUrl()
+        throws Exception
+    {
+        RemoteRepository repo = new RemoteRepository.Builder( "test", "default", "http://host:invalid" ).build();
+        assertNull( selector.getProxy( repo ) );
+    }
+
+    @Test
+    public void testGetProxy_OpaqueUrl()
+        throws Exception
+    {
+        RemoteRepository repo = new RemoteRepository.Builder( "test", "default", "classpath:base" ).build();
+        assertNull( selector.getProxy( repo ) );
+    }
+
+    @Test
+    public void testGetProxy_NullSelector()
+        throws Exception
+    {
+        RemoteRepository repo = new RemoteRepository.Builder( "test", "default", "http://repo.eclipse.org/" ).build();
+        java.net.ProxySelector.setDefault( null );
+        assertNull( selector.getProxy( repo ) );
+    }
+
+    @Test
+    public void testGetProxy_NoProxies()
+        throws Exception
+    {
+        RemoteRepository repo = new RemoteRepository.Builder( "test", "default", "http://repo.eclipse.org/" ).build();
+        java.net.ProxySelector.setDefault( new AbstractProxySelector()
+        {
+            @Override
+            public List<java.net.Proxy> select( URI uri )
+            {
+                return Collections.emptyList();
+            }
+
+        } );
+        assertNull( selector.getProxy( repo ) );
+    }
+
+    @Test
+    public void testGetProxy_DirectProxy()
+        throws Exception
+    {
+        RemoteRepository repo = new RemoteRepository.Builder( "test", "default", "http://repo.eclipse.org/" ).build();
+        final InetSocketAddress addr = InetSocketAddress.createUnresolved( "proxy", 8080 );
+        java.net.ProxySelector.setDefault( new AbstractProxySelector()
+        {
+            @Override
+            public List<java.net.Proxy> select( URI uri )
+            {
+                return Arrays.asList( java.net.Proxy.NO_PROXY, new java.net.Proxy( java.net.Proxy.Type.HTTP, addr ) );
+            }
+
+        } );
+        assertNull( selector.getProxy( repo ) );
+    }
+
+    @Test
+    public void testGetProxy_HttpProxy()
+        throws Exception
+    {
+        final RemoteRepository repo =
+            new RemoteRepository.Builder( "test", "default", "http://repo.eclipse.org/" ).build();
+        final URL url = new URL( repo.getUrl() );
+        final InetSocketAddress addr = InetSocketAddress.createUnresolved( "proxy", 8080 );
+        java.net.ProxySelector.setDefault( new AbstractProxySelector()
+        {
+            @Override
+            public List<java.net.Proxy> select( URI uri )
+            {
+                if ( repo.getHost().equalsIgnoreCase( uri.getHost() ) )
+                {
+                    return Arrays.asList( new java.net.Proxy( java.net.Proxy.Type.HTTP, addr ) );
+                }
+                return Collections.emptyList();
+            }
+
+        } );
+        Authenticator.setDefault( new Authenticator()
+        {
+            @Override
+            protected PasswordAuthentication getPasswordAuthentication()
+            {
+                if ( Authenticator.RequestorType.PROXY.equals( getRequestorType() )
+                    && addr.getHostName().equals( getRequestingHost() ) && addr.getPort() == getRequestingPort()
+                    && url.equals( getRequestingURL() ) )
+                {
+                    return new PasswordAuthentication( "proxyuser", "proxypass".toCharArray() );
+                }
+                return super.getPasswordAuthentication();
+            }
+        } );
+
+        Proxy proxy = selector.getProxy( repo );
+        assertNotNull( proxy );
+        assertEquals( addr.getHostName(), proxy.getHost() );
+        assertEquals( addr.getPort(), proxy.getPort() );
+        assertEquals( Proxy.TYPE_HTTP, proxy.getType() );
+
+        RemoteRepository repo2 = new RemoteRepository.Builder( repo ).setProxy( proxy ).build();
+        Authentication auth = proxy.getAuthentication();
+        assertNotNull( auth );
+        AuthenticationContext authCtx = AuthenticationContext.forProxy( new DefaultRepositorySystemSession(), repo2 );
+        assertEquals( "proxyuser", authCtx.get( AuthenticationContext.USERNAME ) );
+        assertEquals( "proxypass", authCtx.get( AuthenticationContext.PASSWORD ) );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/SecretAuthenticationTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/SecretAuthenticationTest.java
new file mode 100644
index 0000000..df4afaf
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/SecretAuthenticationTest.java
@@ -0,0 +1,109 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+public class SecretAuthenticationTest
+{
+
+    private RepositorySystemSession newSession()
+    {
+        return new DefaultRepositorySystemSession();
+    }
+
+    private RemoteRepository newRepo( Authentication auth )
+    {
+        return new RemoteRepository.Builder( "test", "default", "http://localhost" ).setAuthentication( auth ).build();
+    }
+
+    private AuthenticationContext newContext( Authentication auth )
+    {
+        return AuthenticationContext.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    private String newDigest( Authentication auth )
+    {
+        return AuthenticationDigest.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    @Test
+    public void testConstructor_CopyChars()
+    {
+        char[] value = { 'v', 'a', 'l' };
+        new SecretAuthentication( "key", value );
+        assertArrayEquals( new char[] { 'v', 'a', 'l' }, value );
+    }
+
+    @Test
+    public void testFill()
+    {
+        Authentication auth = new SecretAuthentication( "key", "value" );
+        AuthenticationContext context = newContext( auth );
+        assertEquals( null, context.get( "another-key" ) );
+        assertEquals( "value", context.get( "key" ) );
+    }
+
+    @Test
+    public void testDigest()
+    {
+        Authentication auth1 = new SecretAuthentication( "key", "value" );
+        Authentication auth2 = new SecretAuthentication( "key", "value" );
+        String digest1 = newDigest( auth1 );
+        String digest2 = newDigest( auth2 );
+        assertEquals( digest1, digest2 );
+
+        Authentication auth3 = new SecretAuthentication( "key", "Value" );
+        String digest3 = newDigest( auth3 );
+        assertFalse( digest3.equals( digest1 ) );
+
+        Authentication auth4 = new SecretAuthentication( "Key", "value" );
+        String digest4 = newDigest( auth4 );
+        assertFalse( digest4.equals( digest1 ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        Authentication auth1 = new SecretAuthentication( "key", "value" );
+        Authentication auth2 = new SecretAuthentication( "key", "value" );
+        Authentication auth3 = new SecretAuthentication( "key", "Value" );
+        assertEquals( auth1, auth2 );
+        assertFalse( auth1.equals( auth3 ) );
+        assertFalse( auth1.equals( null ) );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        Authentication auth1 = new SecretAuthentication( "key", "value" );
+        Authentication auth2 = new SecretAuthentication( "key", "value" );
+        assertEquals( auth1.hashCode(), auth2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/StringAuthenticationTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/StringAuthenticationTest.java
new file mode 100644
index 0000000..8f89299
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/StringAuthenticationTest.java
@@ -0,0 +1,101 @@
+package org.eclipse.aether.util.repository;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationContext;
+import org.eclipse.aether.repository.AuthenticationDigest;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+public class StringAuthenticationTest
+{
+
+    private RepositorySystemSession newSession()
+    {
+        return new DefaultRepositorySystemSession();
+    }
+
+    private RemoteRepository newRepo( Authentication auth )
+    {
+        return new RemoteRepository.Builder( "test", "default", "http://localhost" ).setAuthentication( auth ).build();
+    }
+
+    private AuthenticationContext newContext( Authentication auth )
+    {
+        return AuthenticationContext.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    private String newDigest( Authentication auth )
+    {
+        return AuthenticationDigest.forRepository( newSession(), newRepo( auth ) );
+    }
+
+    @Test
+    public void testFill()
+    {
+        Authentication auth = new StringAuthentication( "key", "value" );
+        AuthenticationContext context = newContext( auth );
+        assertEquals( null, context.get( "another-key" ) );
+        assertEquals( "value", context.get( "key" ) );
+    }
+
+    @Test
+    public void testDigest()
+    {
+        Authentication auth1 = new StringAuthentication( "key", "value" );
+        Authentication auth2 = new StringAuthentication( "key", "value" );
+        String digest1 = newDigest( auth1 );
+        String digest2 = newDigest( auth2 );
+        assertEquals( digest1, digest2 );
+
+        Authentication auth3 = new StringAuthentication( "key", "Value" );
+        String digest3 = newDigest( auth3 );
+        assertFalse( digest3.equals( digest1 ) );
+
+        Authentication auth4 = new StringAuthentication( "Key", "value" );
+        String digest4 = newDigest( auth4 );
+        assertFalse( digest4.equals( digest1 ) );
+    }
+
+    @Test
+    public void testEquals()
+    {
+        Authentication auth1 = new StringAuthentication( "key", "value" );
+        Authentication auth2 = new StringAuthentication( "key", "value" );
+        Authentication auth3 = new StringAuthentication( "key", "Value" );
+        assertEquals( auth1, auth2 );
+        assertFalse( auth1.equals( auth3 ) );
+        assertFalse( auth1.equals( null ) );
+    }
+
+    @Test
+    public void testHashCode()
+    {
+        Authentication auth1 = new StringAuthentication( "key", "value" );
+        Authentication auth2 = new StringAuthentication( "key", "value" );
+        assertEquals( auth1.hashCode(), auth2.hashCode() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/AbstractVersionTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/AbstractVersionTest.java
new file mode 100644
index 0000000..52541db
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/AbstractVersionTest.java
@@ -0,0 +1,79 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import org.eclipse.aether.version.Version;
+
+/**
+ */
+abstract class AbstractVersionTest
+{
+
+    protected static final int X_LT_Y = -1;
+
+    protected static final int X_EQ_Y = 0;
+
+    protected static final int X_GT_Y = 1;
+
+    protected abstract Version newVersion( String version );
+
+    protected void assertOrder( int expected, String version1, String version2 )
+    {
+        Version v1 = newVersion( version1 );
+        Version v2 = newVersion( version2 );
+
+        if ( expected > 0 )
+        {
+            assertEquals( "expected " + v1 + " > " + v2, 1, Integer.signum( v1.compareTo( v2 ) ) );
+            assertEquals( "expected " + v2 + " < " + v1, -1, Integer.signum( v2.compareTo( v1 ) ) );
+            assertEquals( "expected " + v1 + " != " + v2, false, v1.equals( v2 ) );
+            assertEquals( "expected " + v2 + " != " + v1, false, v2.equals( v1 ) );
+        }
+        else if ( expected < 0 )
+        {
+            assertEquals( "expected " + v1 + " < " + v2, -1, Integer.signum( v1.compareTo( v2 ) ) );
+            assertEquals( "expected " + v2 + " > " + v1, 1, Integer.signum( v2.compareTo( v1 ) ) );
+            assertEquals( "expected " + v1 + " != " + v2, false, v1.equals( v2 ) );
+            assertEquals( "expected " + v2 + " != " + v1, false, v2.equals( v1 ) );
+        }
+        else
+        {
+            assertEquals( "expected " + v1 + " == " + v2, 0, v1.compareTo( v2 ) );
+            assertEquals( "expected " + v2 + " == " + v1, 0, v2.compareTo( v1 ) );
+            assertEquals( "expected " + v1 + " == " + v2, true, v1.equals( v2 ) );
+            assertEquals( "expected " + v2 + " == " + v1, true, v2.equals( v1 ) );
+            assertEquals( "expected #(" + v1 + ") == #(" + v1 + ")", v1.hashCode(), v2.hashCode() );
+        }
+    }
+
+    protected void assertSequence( String... versions )
+    {
+        for ( int i = 0; i < versions.length - 1; i++ )
+        {
+            for ( int j = i + 1; j < versions.length; j++ )
+            {
+                assertOrder( X_LT_Y, versions[i], versions[j] );
+            }
+        }
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionRangeTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionRangeTest.java
new file mode 100644
index 0000000..85d007f
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionRangeTest.java
@@ -0,0 +1,167 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.util.version.GenericVersion;
+import org.eclipse.aether.util.version.GenericVersionRange;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionRange;
+import org.junit.Test;
+
+public class GenericVersionRangeTest
+{
+
+    private Version newVersion( String version )
+    {
+        return new GenericVersion( version );
+    }
+
+    private VersionRange parseValid( String range )
+    {
+        try
+        {
+            return new GenericVersionRange( range );
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            AssertionError error =
+                new AssertionError( range + " should be valid but failed to parse due to: " + e.getMessage() );
+            error.initCause( e );
+            throw error;
+        }
+    }
+
+    private void parseInvalid( String range )
+    {
+        try
+        {
+            new GenericVersionRange( range );
+            fail( range + " should be invalid" );
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            assertTrue( true );
+        }
+    }
+
+    private void assertContains( VersionRange range, String version )
+    {
+        assertTrue( range + " should contain " + version, range.containsVersion( newVersion( version ) ) );
+    }
+
+    private void assertNotContains( VersionRange range, String version )
+    {
+        assertFalse( range + " should not contain " + version, range.containsVersion( newVersion( version ) ) );
+    }
+
+    @Test
+    public void testLowerBoundInclusiveUpperBoundInclusive()
+    {
+        VersionRange range = parseValid( "[1,2]" );
+        assertContains( range, "1" );
+        assertContains( range, "1.1-SNAPSHOT" );
+        assertContains( range, "2" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testLowerBoundInclusiveUpperBoundExclusive()
+    {
+        VersionRange range = parseValid( "[1.2.3.4.5,1.2.3.4.6)" );
+        assertContains( range, "1.2.3.4.5" );
+        assertNotContains( range, "1.2.3.4.6" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testLowerBoundExclusiveUpperBoundInclusive()
+    {
+        VersionRange range = parseValid( "(1a,1b]" );
+        assertNotContains( range, "1a" );
+        assertContains( range, "1b" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testLowerBoundExclusiveUpperBoundExclusive()
+    {
+        VersionRange range = parseValid( "(1,3)" );
+        assertNotContains( range, "1" );
+        assertContains( range, "2-SNAPSHOT" );
+        assertNotContains( range, "3" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testSingleVersion()
+    {
+        VersionRange range = parseValid( "[1]" );
+        assertContains( range, "1" );
+        assertEquals( range, parseValid( range.toString() ) );
+
+        range = parseValid( "[1,1]" );
+        assertContains( range, "1" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testSingleWildcardVersion()
+    {
+        VersionRange range = parseValid( "[1.2.*]" );
+        assertContains( range, "1.2-alpha-1" );
+        assertContains( range, "1.2-SNAPSHOT" );
+        assertContains( range, "1.2" );
+        assertContains( range, "1.2.9999999" );
+        assertNotContains( range, "1.3-rc-1" );
+        assertEquals( range, parseValid( range.toString() ) );
+    }
+
+    @Test
+    public void testMissingOpenCloseDelimiter()
+    {
+        parseInvalid( "1.0" );
+    }
+
+    @Test
+    public void testMissingOpenDelimiter()
+    {
+        parseInvalid( "1.0]" );
+        parseInvalid( "1.0)" );
+    }
+
+    @Test
+    public void testMissingCloseDelimiter()
+    {
+        parseInvalid( "[1.0" );
+        parseInvalid( "(1.0" );
+    }
+
+    @Test
+    public void testTooManyVersions()
+    {
+        parseInvalid( "[1,2,3]" );
+        parseInvalid( "(1,2,3)" );
+        parseInvalid( "[1,2,3)" );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionSchemeTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionSchemeTest.java
new file mode 100644
index 0000000..f52f73d
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionSchemeTest.java
@@ -0,0 +1,113 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import org.eclipse.aether.util.version.GenericVersion;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.VersionConstraint;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ */
+public class GenericVersionSchemeTest
+{
+
+    private GenericVersionScheme scheme;
+
+    @Before
+    public void setUp()
+        throws Exception
+    {
+        scheme = new GenericVersionScheme();
+    }
+
+    private InvalidVersionSpecificationException parseInvalid( String constraint )
+    {
+        try
+        {
+            scheme.parseVersionConstraint( constraint );
+            fail( "expected exception for constraint " + constraint );
+            return null;
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            return e;
+        }
+    }
+
+    @Test
+    public void testEnumeratedVersions()
+        throws InvalidVersionSpecificationException
+    {
+        VersionConstraint c = scheme.parseVersionConstraint( "1.0" );
+        assertEquals( "1.0", c.getVersion().toString() );
+        assertTrue( c.containsVersion( new GenericVersion( "1.0" ) ) );
+
+        c = scheme.parseVersionConstraint( "[1.0]" );
+        assertEquals( null, c.getVersion() );
+        assertTrue( c.containsVersion( new GenericVersion( "1.0" ) ) );
+
+        c = scheme.parseVersionConstraint( "[1.0],[2.0]" );
+        assertTrue( c.containsVersion( new GenericVersion( "1.0" ) ) );
+        assertTrue( c.containsVersion( new GenericVersion( "2.0" ) ) );
+
+        c = scheme.parseVersionConstraint( "[1.0],[2.0],[3.0]" );
+        assertContains( c, "1.0", "2.0", "3.0" );
+        assertNotContains( c, "1.5" );
+
+        c = scheme.parseVersionConstraint( "[1,3),(3,5)" );
+        assertContains( c, "1", "2", "4" );
+        assertNotContains( c, "3", "5" );
+
+        c = scheme.parseVersionConstraint( "[1,3),(3,)" );
+        assertContains( c, "1", "2", "4" );
+        assertNotContains( c, "3" );
+    }
+
+    private void assertNotContains( VersionConstraint c, String... versions )
+    {
+        assertContains( String.format( "%s: %%s should not be contained\n", c.toString() ), c, false, versions );
+    }
+
+    private void assertContains( String msg, VersionConstraint c, boolean b, String... versions )
+    {
+        for ( String v : versions )
+        {
+            assertEquals( String.format( msg, v ), b, c.containsVersion( new GenericVersion( v ) ) );
+        }
+    }
+
+    private void assertContains( VersionConstraint c, String... versions )
+    {
+        assertContains( String.format( "%s: %%s should be contained\n", c.toString() ), c, true, versions );
+    }
+
+    @Test
+    public void testInvalid()
+    {
+        parseInvalid( "[1," );
+        parseInvalid( "[1,2],(3," );
+        parseInvalid( "[1,2],3" );
+    }
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionTest.java
new file mode 100644
index 0000000..ae891af
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/GenericVersionTest.java
@@ -0,0 +1,345 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import java.util.Locale;
+
+import org.eclipse.aether.util.version.GenericVersion;
+import org.eclipse.aether.version.Version;
+import org.junit.Test;
+
+/**
+ */
+public class GenericVersionTest
+    extends AbstractVersionTest
+{
+
+    protected Version newVersion( String version )
+    {
+        return new GenericVersion( version );
+    }
+
+    @Test
+    public void testEmptyVersion()
+    {
+        assertOrder( X_EQ_Y, "0", "" );
+    }
+
+    @Test
+    public void testNumericOrdering()
+    {
+        assertOrder( X_LT_Y, "2", "10" );
+        assertOrder( X_LT_Y, "1.2", "1.10" );
+        assertOrder( X_LT_Y, "1.0.2", "1.0.10" );
+        assertOrder( X_LT_Y, "1.0.0.2", "1.0.0.10" );
+        assertOrder( X_LT_Y, "1.0.20101206.111434.1", "1.0.20101206.111435.1" );
+        assertOrder( X_LT_Y, "1.0.20101206.111434.2", "1.0.20101206.111434.10" );
+    }
+
+    @Test
+    public void testDelimiters()
+    {
+        assertOrder( X_EQ_Y, "1.0", "1-0" );
+        assertOrder( X_EQ_Y, "1.0", "1_0" );
+        assertOrder( X_EQ_Y, "1.a", "1a" );
+    }
+
+    @Test
+    public void testLeadingZerosAreSemanticallyIrrelevant()
+    {
+        assertOrder( X_EQ_Y, "1", "01" );
+        assertOrder( X_EQ_Y, "1.2", "1.002" );
+        assertOrder( X_EQ_Y, "1.2.3", "1.2.0003" );
+        assertOrder( X_EQ_Y, "1.2.3.4", "1.2.3.00004" );
+    }
+
+    @Test
+    public void testTrailingZerosAreSemanticallyIrrelevant()
+    {
+        assertOrder( X_EQ_Y, "1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0" );
+        assertOrder( X_EQ_Y, "1", "1-0-0-0-0-0-0-0-0-0-0-0-0-0" );
+        assertOrder( X_EQ_Y, "1", "1.0-0.0-0.0-0.0-0.0-0.0-0.0" );
+        assertOrder( X_EQ_Y, "1", "1.0000000000000" );
+        assertOrder( X_EQ_Y, "1.0", "1.0.0" );
+    }
+
+    @Test
+    public void testTrailingZerosBeforeQualifierAreSemanticallyIrrelevant()
+    {
+        assertOrder( X_EQ_Y, "1.0-ga", "1.0.0-ga" );
+        assertOrder( X_EQ_Y, "1.0.ga", "1.0.0.ga" );
+        assertOrder( X_EQ_Y, "1.0ga", "1.0.0ga" );
+
+        assertOrder( X_EQ_Y, "1.0-alpha", "1.0.0-alpha" );
+        assertOrder( X_EQ_Y, "1.0.alpha", "1.0.0.alpha" );
+        assertOrder( X_EQ_Y, "1.0alpha", "1.0.0alpha" );
+        assertOrder( X_EQ_Y, "1.0-alpha-snapshot", "1.0.0-alpha-snapshot" );
+        assertOrder( X_EQ_Y, "1.0.alpha.snapshot", "1.0.0.alpha.snapshot" );
+
+        assertOrder( X_EQ_Y, "1.x.0-alpha", "1.x.0.0-alpha" );
+        assertOrder( X_EQ_Y, "1.x.0.alpha", "1.x.0.0.alpha" );
+        assertOrder( X_EQ_Y, "1.x.0-alpha-snapshot", "1.x.0.0-alpha-snapshot" );
+        assertOrder( X_EQ_Y, "1.x.0.alpha.snapshot", "1.x.0.0.alpha.snapshot" );
+    }
+
+    @Test
+    public void testTrailingDelimitersAreSemanticallyIrrelevant()
+    {
+        assertOrder( X_EQ_Y, "1", "1............." );
+        assertOrder( X_EQ_Y, "1", "1-------------" );
+        assertOrder( X_EQ_Y, "1.0", "1............." );
+        assertOrder( X_EQ_Y, "1.0", "1-------------" );
+    }
+
+    @Test
+    public void testInitialDelimiters()
+    {
+        assertOrder( X_EQ_Y, "0.1", ".1" );
+        assertOrder( X_EQ_Y, "0.0.1", "..1" );
+        assertOrder( X_EQ_Y, "0.1", "-1" );
+        assertOrder( X_EQ_Y, "0.0.1", "--1" );
+    }
+
+    @Test
+    public void testConsecutiveDelimiters()
+    {
+        assertOrder( X_EQ_Y, "1.0.1", "1..1" );
+        assertOrder( X_EQ_Y, "1.0.0.1", "1...1" );
+        assertOrder( X_EQ_Y, "1.0.1", "1--1" );
+        assertOrder( X_EQ_Y, "1.0.0.1", "1---1" );
+    }
+
+    @Test
+    public void testUnlimitedNumberOfVersionComponents()
+    {
+        assertOrder( X_GT_Y, "1.0.1.2.3.4.5.6.7.8.9.0.1.2.10", "1.0.1.2.3.4.5.6.7.8.9.0.1.2.3" );
+    }
+
+    @Test
+    public void testUnlimitedNumberOfDigitsInNumericComponent()
+    {
+        assertOrder( X_GT_Y, "1.1234567890123456789012345678901", "1.123456789012345678901234567891" );
+    }
+
+    @Test
+    public void testTransitionFromDigitToLetterAndViceVersaIsEqualivantToDelimiter()
+    {
+        assertOrder( X_EQ_Y, "1alpha10", "1.alpha.10" );
+        assertOrder( X_EQ_Y, "1alpha10", "1-alpha-10" );
+
+        assertOrder( X_GT_Y, "1.alpha10", "1.alpha2" );
+        assertOrder( X_GT_Y, "10alpha", "1alpha" );
+    }
+
+    @Test
+    public void testWellKnownQualifierOrdering()
+    {
+        assertOrder( X_EQ_Y, "1-alpha1", "1-a1" );
+        assertOrder( X_LT_Y, "1-alpha", "1-beta" );
+        assertOrder( X_EQ_Y, "1-beta1", "1-b1" );
+        assertOrder( X_LT_Y, "1-beta", "1-milestone" );
+        assertOrder( X_EQ_Y, "1-milestone1", "1-m1" );
+        assertOrder( X_LT_Y, "1-milestone", "1-rc" );
+        assertOrder( X_EQ_Y, "1-rc", "1-cr" );
+        assertOrder( X_LT_Y, "1-rc", "1-snapshot" );
+        assertOrder( X_LT_Y, "1-snapshot", "1" );
+        assertOrder( X_EQ_Y, "1", "1-ga" );
+        assertOrder( X_EQ_Y, "1", "1.ga.0.ga" );
+        assertOrder( X_EQ_Y, "1.0", "1-ga" );
+        assertOrder( X_EQ_Y, "1", "1-ga.ga" );
+        assertOrder( X_EQ_Y, "1", "1-ga-ga" );
+        assertOrder( X_EQ_Y, "A", "A.ga.ga" );
+        assertOrder( X_EQ_Y, "A", "A-ga-ga" );
+        assertOrder( X_EQ_Y, "1", "1-final" );
+        assertOrder( X_LT_Y, "1", "1-sp" );
+
+        assertOrder( X_LT_Y, "A.rc.1", "A.ga.1" );
+        assertOrder( X_GT_Y, "A.sp.1", "A.ga.1" );
+        assertOrder( X_LT_Y, "A.rc.x", "A.ga.x" );
+        assertOrder( X_GT_Y, "A.sp.x", "A.ga.x" );
+    }
+
+    @Test
+    public void testWellKnownQualifierVersusUnknownQualifierOrdering()
+    {
+        assertOrder( X_GT_Y, "1-abc", "1-alpha" );
+        assertOrder( X_GT_Y, "1-abc", "1-beta" );
+        assertOrder( X_GT_Y, "1-abc", "1-milestone" );
+        assertOrder( X_GT_Y, "1-abc", "1-rc" );
+        assertOrder( X_GT_Y, "1-abc", "1-snapshot" );
+        assertOrder( X_GT_Y, "1-abc", "1" );
+        assertOrder( X_GT_Y, "1-abc", "1-sp" );
+    }
+
+    @Test
+    public void testWellKnownSingleCharQualifiersOnlyRecognizedIfImmediatelyFollowedByNumber()
+    {
+        assertOrder( X_GT_Y, "1.0a", "1.0" );
+        assertOrder( X_GT_Y, "1.0-a", "1.0" );
+        assertOrder( X_GT_Y, "1.0.a", "1.0" );
+        assertOrder( X_GT_Y, "1.0b", "1.0" );
+        assertOrder( X_GT_Y, "1.0-b", "1.0" );
+        assertOrder( X_GT_Y, "1.0.b", "1.0" );
+        assertOrder( X_GT_Y, "1.0m", "1.0" );
+        assertOrder( X_GT_Y, "1.0-m", "1.0" );
+        assertOrder( X_GT_Y, "1.0.m", "1.0" );
+
+        assertOrder( X_LT_Y, "1.0a1", "1.0" );
+        assertOrder( X_LT_Y, "1.0-a1", "1.0" );
+        assertOrder( X_LT_Y, "1.0.a1", "1.0" );
+        assertOrder( X_LT_Y, "1.0b1", "1.0" );
+        assertOrder( X_LT_Y, "1.0-b1", "1.0" );
+        assertOrder( X_LT_Y, "1.0.b1", "1.0" );
+        assertOrder( X_LT_Y, "1.0m1", "1.0" );
+        assertOrder( X_LT_Y, "1.0-m1", "1.0" );
+        assertOrder( X_LT_Y, "1.0.m1", "1.0" );
+
+        assertOrder( X_GT_Y, "1.0a.1", "1.0" );
+        assertOrder( X_GT_Y, "1.0a-1", "1.0" );
+        assertOrder( X_GT_Y, "1.0b.1", "1.0" );
+        assertOrder( X_GT_Y, "1.0b-1", "1.0" );
+        assertOrder( X_GT_Y, "1.0m.1", "1.0" );
+        assertOrder( X_GT_Y, "1.0m-1", "1.0" );
+    }
+
+    @Test
+    public void testUnknownQualifierOrdering()
+    {
+        assertOrder( X_LT_Y, "1-abc", "1-abcd" );
+        assertOrder( X_LT_Y, "1-abc", "1-bcd" );
+        assertOrder( X_GT_Y, "1-abc", "1-aac" );
+    }
+
+    @Test
+    public void testCaseInsensitiveOrderingOfQualifiers()
+    {
+        assertOrder( X_EQ_Y, "1.alpha", "1.ALPHA" );
+        assertOrder( X_EQ_Y, "1.alpha", "1.Alpha" );
+
+        assertOrder( X_EQ_Y, "1.beta", "1.BETA" );
+        assertOrder( X_EQ_Y, "1.beta", "1.Beta" );
+
+        assertOrder( X_EQ_Y, "1.milestone", "1.MILESTONE" );
+        assertOrder( X_EQ_Y, "1.milestone", "1.Milestone" );
+
+        assertOrder( X_EQ_Y, "1.rc", "1.RC" );
+        assertOrder( X_EQ_Y, "1.rc", "1.Rc" );
+        assertOrder( X_EQ_Y, "1.cr", "1.CR" );
+        assertOrder( X_EQ_Y, "1.cr", "1.Cr" );
+
+        assertOrder( X_EQ_Y, "1.snapshot", "1.SNAPSHOT" );
+        assertOrder( X_EQ_Y, "1.snapshot", "1.Snapshot" );
+
+        assertOrder( X_EQ_Y, "1.ga", "1.GA" );
+        assertOrder( X_EQ_Y, "1.ga", "1.Ga" );
+        assertOrder( X_EQ_Y, "1.final", "1.FINAL" );
+        assertOrder( X_EQ_Y, "1.final", "1.Final" );
+
+        assertOrder( X_EQ_Y, "1.sp", "1.SP" );
+        assertOrder( X_EQ_Y, "1.sp", "1.Sp" );
+
+        assertOrder( X_EQ_Y, "1.unknown", "1.UNKNOWN" );
+        assertOrder( X_EQ_Y, "1.unknown", "1.Unknown" );
+    }
+
+    @Test
+    public void testCaseInsensitiveOrderingOfQualifiersIsLocaleIndependent()
+    {
+        Locale orig = Locale.getDefault();
+        try
+        {
+            Locale[] locales = { Locale.ENGLISH, new Locale( "tr" ) };
+            for ( Locale locale : locales )
+            {
+                Locale.setDefault( locale );
+                assertOrder( X_EQ_Y, "1-abcdefghijklmnopqrstuvwxyz", "1-ABCDEFGHIJKLMNOPQRSTUVWXYZ" );
+            }
+        }
+        finally
+        {
+            Locale.setDefault( orig );
+        }
+    }
+
+    @Test
+    public void testQualifierVersusNumberOrdering()
+    {
+        assertOrder( X_LT_Y, "1-ga", "1-1" );
+        assertOrder( X_LT_Y, "1.ga", "1.1" );
+        assertOrder( X_EQ_Y, "1-ga", "1.0" );
+        assertOrder( X_EQ_Y, "1.ga", "1.0" );
+
+        assertOrder( X_LT_Y, "1-ga-1", "1-0-1" );
+        assertOrder( X_LT_Y, "1.ga.1", "1.0.1" );
+
+        assertOrder( X_GT_Y, "1.sp", "1.0" );
+        assertOrder( X_LT_Y, "1.sp", "1.1" );
+
+        assertOrder( X_LT_Y, "1-abc", "1-1" );
+        assertOrder( X_LT_Y, "1.abc", "1.1" );
+
+        assertOrder( X_LT_Y, "1-xyz", "1-1" );
+        assertOrder( X_LT_Y, "1.xyz", "1.1" );
+    }
+
+    @Test
+    public void testVersionEvolution()
+    {
+        assertSequence( "0.9.9-SNAPSHOT", "0.9.9", "0.9.10-SNAPSHOT", "0.9.10", "1.0-alpha-2-SNAPSHOT", "1.0-alpha-2",
+                        "1.0-alpha-10-SNAPSHOT", "1.0-alpha-10", "1.0-beta-1-SNAPSHOT", "1.0-beta-1",
+                        "1.0-rc-1-SNAPSHOT", "1.0-rc-1", "1.0-SNAPSHOT", "1.0", "1.0-sp-1-SNAPSHOT", "1.0-sp-1",
+                        "1.0.1-alpha-1-SNAPSHOT", "1.0.1-alpha-1", "1.0.1-beta-1-SNAPSHOT", "1.0.1-beta-1",
+                        "1.0.1-rc-1-SNAPSHOT", "1.0.1-rc-1", "1.0.1-SNAPSHOT", "1.0.1", "1.1-SNAPSHOT", "1.1" );
+
+        assertSequence( "1.0-alpha", "1.0", "1.0-1" );
+        assertSequence( "1.0.alpha", "1.0", "1.0-1" );
+        assertSequence( "1.0-alpha", "1.0", "1.0.1" );
+        assertSequence( "1.0.alpha", "1.0", "1.0.1" );
+    }
+
+    @Test
+    public void testMinimumSegment()
+    {
+        assertOrder( X_LT_Y, "1.min", "1.0-alpha-1" );
+        assertOrder( X_LT_Y, "1.min", "1.0-SNAPSHOT" );
+        assertOrder( X_LT_Y, "1.min", "1.0" );
+        assertOrder( X_LT_Y, "1.min", "1.9999999999" );
+
+        assertOrder( X_EQ_Y, "1.min", "1.MIN" );
+
+        assertOrder( X_GT_Y, "1.min", "0.99999" );
+        assertOrder( X_GT_Y, "1.min", "0.max" );
+    }
+
+    @Test
+    public void testMaximumSegment()
+    {
+        assertOrder( X_GT_Y, "1.max", "1.0-alpha-1" );
+        assertOrder( X_GT_Y, "1.max", "1.0-SNAPSHOT" );
+        assertOrder( X_GT_Y, "1.max", "1.0" );
+        assertOrder( X_GT_Y, "1.max", "1.9999999999" );
+
+        assertOrder( X_EQ_Y, "1.max", "1.MAX" );
+
+        assertOrder( X_LT_Y, "1.max", "2.0-alpha-1" );
+        assertOrder( X_LT_Y, "1.max", "2.min" );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/UnionVersionRangeTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/UnionVersionRangeTest.java
new file mode 100644
index 0000000..570b6b7
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/version/UnionVersionRangeTest.java
@@ -0,0 +1,105 @@
+package org.eclipse.aether.util.version;
+
+/*
+ * 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.
+ */
+
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.VersionRange;
+import org.junit.Test;
+
+public class UnionVersionRangeTest
+{
+
+    private VersionRange newRange( String range )
+    {
+        try
+        {
+            return new GenericVersionScheme().parseVersionRange( range );
+        }
+        catch ( InvalidVersionSpecificationException e )
+        {
+            throw new IllegalArgumentException( e );
+        }
+    }
+
+    private void assertBound( String version, boolean inclusive, VersionRange.Bound bound )
+    {
+        if ( version == null )
+        {
+            assertNull( bound );
+        }
+        else
+        {
+            assertNotNull( bound );
+            assertNotNull( bound.getVersion() );
+            assertEquals( inclusive, bound.isInclusive() );
+            try
+            {
+                assertEquals( new GenericVersionScheme().parseVersion( version ), bound.getVersion() );
+            }
+            catch ( InvalidVersionSpecificationException e )
+            {
+                throw new IllegalArgumentException( e );
+            }
+        }
+    }
+
+    @Test
+    public void testGetLowerBound()
+    {
+        VersionRange range = UnionVersionRange.from( Collections.<VersionRange> emptySet() );
+        assertBound( null, false, range.getLowerBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "[3,4]" ) );
+        assertBound( "1", true, range.getLowerBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "(,4]" ) );
+        assertBound( null, false, range.getLowerBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "(1,4]" ) );
+        assertBound( "1", true, range.getLowerBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "(0,4]" ) );
+        assertBound( "0", false, range.getLowerBound() );
+    }
+
+    @Test
+    public void testGetUpperBound()
+    {
+        VersionRange range = UnionVersionRange.from( Collections.<VersionRange> emptySet() );
+        assertBound( null, false, range.getUpperBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "[3,4]" ) );
+        assertBound( "4", true, range.getUpperBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "[3,)" ) );
+        assertBound( null, false, range.getUpperBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "[1,2)" ) );
+        assertBound( "2", true, range.getUpperBound() );
+
+        range = UnionVersionRange.from( newRange( "[1,2]" ), newRange( "[1,3)" ) );
+        assertBound( "3", false, range.getUpperBound() );
+    }
+
+}
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycle.txt b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycle.txt
new file mode 100644
index 0000000..1c200b9
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycle.txt
@@ -0,0 +1,5 @@
+(null)
++- gid:aid:ver
+|  \- gid2:aid:ver
+\- gid2:aid:ver
+   \- gid:aid:ver
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycles.txt b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycles.txt
new file mode 100644
index 0000000..714069e
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/cycles.txt
@@ -0,0 +1,14 @@
+(null)
++- gid1:aid:ver
+|  \- gid2:aid:ver
+|     \- gid:aid:ver
++- gid2:aid:ver
+|  \- gid1:aid:ver
++- gid1:aid:ver
+|  \- gid3:aid:ver
++- gid3:aid:ver
+|  \- gid1:aid:ver
++- gid2:aid:ver
+|  \- gid3:aid:ver
+\- gid3:aid:ver
+   \- gid2:aid:ver
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/no-conflicts.txt b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/no-conflicts.txt
new file mode 100644
index 0000000..c4d1c89
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/no-conflicts.txt
@@ -0,0 +1,5 @@
+(null)
++- gid:aid:ver
+|  \- gid2:aid:ver
+\- gid3:aid:ver
+   \- gid4:aid:ver
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/simple.txt b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/simple.txt
new file mode 100644
index 0000000..01ce915
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-id-sorter/simple.txt
@@ -0,0 +1,5 @@
+(null)
++- gid:aid:ver
+|  \- gid:aid2:ver
+\- gid2:aid:ver
+   \- gid:aid:ver
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation1.txt b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation1.txt
new file mode 100644
index 0000000..518f706
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation1.txt
@@ -0,0 +1,3 @@
+(null)
++- test:a:1
+\- test:a:1 relocations=test:reloc:1
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation2.txt b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation2.txt
new file mode 100644
index 0000000..729748d
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation2.txt
@@ -0,0 +1,3 @@
+(null)
++- test:a:1 relocations=test:reloc:1
+\- test:a:1
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation3.txt b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation3.txt
new file mode 100644
index 0000000..8b96d18
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-marker/relocation3.txt
@@ -0,0 +1,4 @@
+(null)
++- test:a:1
++- test:b:1
+\- test:c:1 relocations=test:a:1,test:b:1
diff --git a/maven-resolver-util/src/test/resources/transformer/conflict-marker/simple.txt b/maven-resolver-util/src/test/resources/transformer/conflict-marker/simple.txt
new file mode 100644
index 0000000..2f94bb4
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/conflict-marker/simple.txt
@@ -0,0 +1,3 @@
+(null)
++- test:a:1
+\- test:b:1
diff --git a/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict-direct-dep.txt b/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict-direct-dep.txt
new file mode 100644
index 0000000..13ac2aa
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict-direct-dep.txt
@@ -0,0 +1,4 @@
+(null)
++- test:a:1
+|  \- test:x:1
+\- test:x:1 optional
diff --git a/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict.txt b/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict.txt
new file mode 100644
index 0000000..ffab99b
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/optionality-selector/conflict.txt
@@ -0,0 +1,5 @@
+(null)
++- test:a:1 optional
+|  \- test:x:1
+\- test:b:1
+   \- test:x:1
diff --git a/maven-resolver-util/src/test/resources/transformer/optionality-selector/derive.txt b/maven-resolver-util/src/test/resources/transformer/optionality-selector/derive.txt
new file mode 100644
index 0000000..37a394a
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/optionality-selector/derive.txt
@@ -0,0 +1,5 @@
+(null)
++- test:a:1 optional
+|  \- test:b:1
+\- test:c:1 !optional
+   \- test:d:1
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflict-and-inheritance.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflict-and-inheritance.txt
new file mode 100644
index 0000000..558049c
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflict-and-inheritance.txt
@@ -0,0 +1,10 @@
+# Another scenario to check that scope inheritance considers the effective scopes of parent nodes as determined during
+# scope conflict resolution. In this example, gid:x:1 should end up in compile scope (and not runtime) because its
+# parent gid:c:2 will be promoted to compile scope due to a conflict with gid:c:1.
+
+gid:root:1
++- gid:a:1 compile
+|  \- gid:c:2 runtime
+|     \- gid:x:1 compile
+\- gid:b:1 compile
+   \- gid:c:1 compile
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflicting-direct-nodes.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflicting-direct-nodes.txt
new file mode 100644
index 0000000..3330251
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/conflicting-direct-nodes.txt
@@ -0,0 +1,3 @@
+(null)
++- gid:aid:ver %s
+\- gid:aid:ver %s
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-a.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-a.txt
new file mode 100644
index 0000000..df2d828
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-a.txt
@@ -0,0 +1,8 @@
+# Checks for graceful handling of cycles in the graph of conflict groups. Below, the group {a:1, a:2} depends on
+# {b:1, b:2} and vice versa. Additionally, each group contains a direct dependency.
+
+gid:root:1
++- gid:a:1 compile
+|  \- gid:b:1 compile
+\- gid:b:2 runtime
+   \- gid:a:2 runtime
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-b.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-b.txt
new file mode 100644
index 0000000..5fe084a
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-b.txt
@@ -0,0 +1,7 @@
+# Variation of cycle-a where the order of direct dependencies has been changed.
+
+gid:root:1
++- gid:b:2 runtime
+|  \- gid:a:2 runtime
+\- gid:a:1 compile
+   \- gid:b:1 compile
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-c.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-c.txt
new file mode 100644
index 0000000..d3495b2
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-c.txt
@@ -0,0 +1,10 @@
+# Checks for graceful handling of cycles in the graph of conflict groups. Below, the group {a:1} depends on
+# {b:1} and vice versa. The conflicting groups consist entirely of non-direct dependencies.
+
+gid:root:1
++- gid:x:1 runtime
+|  \- gid:a:1 compile
+|     \- gid:b:1 compile
+\- gid:y:1 runtime
+   \- gid:b:1 compile
+      \- gid:a:1 compile
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-d.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-d.txt
new file mode 100644
index 0000000..c3a6824
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/cycle-d.txt
@@ -0,0 +1,7 @@
+# Checks for graceful handling of cycles in the graph of dependency nodes.
+
+gid:root:1
+\- gid:a:1 compile
+   \- gid:b:1 compile        (b)
+      \- gid:a:1 runtime
+         \- ^b
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-nodes-winning.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-nodes-winning.txt
new file mode 100644
index 0000000..67edf3d
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-nodes-winning.txt
@@ -0,0 +1,10 @@
+(null)
++- gid:aid:ver %s
++- gid:aid2:ver
+|  \- gid:aid:ver provided
++- gid:aid3:ver
+|  \- gid:aid:ver runtime
++- gid:aid4:ver
+|  \- gid:aid:ver test
+\- gid:aid5:ver
+   \- gid:aid:ver compile
\ No newline at end of file
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-with-conflict-and-inheritance.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-with-conflict-and-inheritance.txt
new file mode 100644
index 0000000..c883214
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/direct-with-conflict-and-inheritance.txt
@@ -0,0 +1,11 @@
+# When a direct dependency scope conflicts with a transitive dependency scope, the direct dependency scope always wins.
+# When the scope of the transitive dependency is updated, this update needs to be considered by scope inheritance.
+# In the graph below gid:a:1 has a conflict, after its resolution to test scope, gid:x:1 should end up in
+# test scope as well, everywhere in the graph.
+
+gid:root:1
++- gid:a:1 test
+|  \- gid:x:1 compile
+\- gid:b:1 compile
+   \- gid:a:1 compile
+      \- gid:x:1 compile
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/dueling-scopes.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/dueling-scopes.txt
new file mode 100644
index 0000000..d2ae8e6
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/dueling-scopes.txt
@@ -0,0 +1,7 @@
+# pattern to test scope mediation in conflict groups
+
+(null)
++- gid:aid2:ver
+|  \- gid:aid:ver %s
+\- gid:aid3:ver
+   \- gid:aid:ver %s
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/inheritance.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/inheritance.txt
new file mode 100644
index 0000000..3b832e9
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/inheritance.txt
@@ -0,0 +1,3 @@
+root:a:ver
+\- gid:b:ver %s
+   \- gid:c:ver %s
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/multiple-inheritance.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/multiple-inheritance.txt
new file mode 100644
index 0000000..d545ed6
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/multiple-inheritance.txt
@@ -0,0 +1,5 @@
+(null)
++- gid:aid:ver %s
+|  \- gid2:aid:ver compile   (1)
+\- gid3:aid:ver %s
+   \- ^1
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-1.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-1.txt
new file mode 100644
index 0000000..61d8659
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-1.txt
@@ -0,0 +1,3 @@
+gid:aid:1
++- gid:aid2:2 compile
+\- gid:aid2:3 system
diff --git a/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-2.txt b/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-2.txt
new file mode 100644
index 0000000..04a5d59
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/scope-calculator/system-2.txt
@@ -0,0 +1,3 @@
+gid:aid:1
++- gid:aid2:2 system
+\- gid:aid2:3 compile
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/conflict-id-cycle.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/conflict-id-cycle.txt
new file mode 100644
index 0000000..57c5d56
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/conflict-id-cycle.txt
@@ -0,0 +1,7 @@
+# a graph which itself is acyclic but its conflict ids are cyclic (a <-> b)
+
+test:root:1
++- test:a:1
+|  \- test:b:1
+\- test:b:2
+   \- test:a:2
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/cycle.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/cycle.txt
new file mode 100644
index 0000000..35c1c6a
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/cycle.txt
@@ -0,0 +1,8 @@
+cycle:root:1
++- cycle:a:1
+|  \- cycle:b:1                   (b)
+|     \- cycle:c:1
+|        \- cycle:a:1             (a)
+|           \- ^b
+\- cycle:c:1
+   \- ^a
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/dead-conflict-group.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/dead-conflict-group.txt
new file mode 100644
index 0000000..cb49d9c
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/dead-conflict-group.txt
@@ -0,0 +1,5 @@
+test:root:1
++- test:a:1
+|  \- test:b:1
+|     \- test:c:1     # conflict group c will completely vanish from resolved tree
+\- test:b:2
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/loop.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/loop.txt
new file mode 100644
index 0000000..2b50f09
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/loop.txt
@@ -0,0 +1,2 @@
+gid:a:1
+\- gid:a:1
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-a.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-a.txt
new file mode 100644
index 0000000..c566caf
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-a.txt
@@ -0,0 +1,9 @@
+test:root:1
++- test:a:1
+|  \- test:b:1           # will be removed in favor of test:b:2
+|     \- test:j:1        # nearest version of j in dirty tree
++- test:c:1
+|  \- test:d:1
+|     \- test:e:1
+|        \- test:j:1
+\- test:b:2
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-b.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-b.txt
new file mode 100644
index 0000000..07867cb
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/nearest-underneath-loser-b.txt
@@ -0,0 +1,9 @@
+test:root:1
++- test:a:1
+|  \- test:b:1           # will be removed in favor of test:b:2
+|     \- test:j:1        # nearest version of j in dirty tree
++- test:c:1
+|  \- test:d:1
+|     \- test:e:1
+|        \- test:j:2
+\- test:b:2
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/overlapping-cycles.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/overlapping-cycles.txt
new file mode 100644
index 0000000..fa9e5aa
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/overlapping-cycles.txt
@@ -0,0 +1,8 @@
+cycle:root:1
++- cycle:a:1                 (a)
+|  \- cycle:b:1
+|     \- cycle:c:1
+|        \- ^a
+\- cycle:b:1                 (b)
+   \- cycle:c:1
+      \- ^b
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/range-backtracking.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/range-backtracking.txt
new file mode 100644
index 0000000..6634e7f
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/range-backtracking.txt
@@ -0,0 +1,10 @@
+(null)
++- test:x:1
++- test:a:1
+|  \- test:b:1
+|     \- test:x:3
++- test:c:1
+|  \- test:x:2
+\- test:d:1
+   \- test:e:1
+      \- test:x:2[2,)   # forces rejection of x:1, should fallback to nearest and not first-seen, i.e. x:2 and not x:3
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/ranges.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/ranges.txt
new file mode 100644
index 0000000..9acf341
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/ranges.txt
@@ -0,0 +1,7 @@
+(null)
++- test:b:1
+|  \- test:a:2[2]
+\- test:c:1
+   +- test:a:1[1,3]
+   +- test:a:2[1,3]
+   \- test:a:3[1,3]
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/scope-vs-version.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/scope-vs-version.txt
new file mode 100644
index 0000000..6c79cfe
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/scope-vs-version.txt
@@ -0,0 +1,14 @@
+# This highlights a design flaw in the previous separation of JavaEffectiveScopeCalculator and NearestVersionConflictResolver:
+# scope conflicts can't be properly determined and resolved until ancestor dependencies got their version conflicts resolved.
+# Otherwise, dependencies can get promoted to a scope due to a scope conflict which actually no longer arises after conflicting
+# versions got removed. In the dirty graph below, the effective scope of test:y should be "test" and not "compile" (as suggested
+# by its test:x parent).
+
+test:root:1
++- test:a:1 compile
+|  +- test:x:1 compile             # (a)
++- test:b:1 test
+|  +- test:y:1 compile
++- test:c:1 test
+   +- test:x:1 compile             # conflicts with (a), hence leaving scope as "compile"
+      +- test:y:1 compile          # since our parent gets removed in favor of (a), no need to promote y into scope "compile"
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-versions.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-versions.txt
new file mode 100644
index 0000000..5d50302
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-versions.txt
@@ -0,0 +1,7 @@
+# multiple versions of the same GA beneath the same parent as seen after expansion of version ranges
+# versions neither in ascending nor descending order
+
+test:root:1
++- test:a:1
++- test:a:3
+\- test:a:2
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/soft-vs-range.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/soft-vs-range.txt
new file mode 100644
index 0000000..d3b10e9
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/soft-vs-range.txt
@@ -0,0 +1,5 @@
+test:root:1
++- test:a:1
+|  \- test:c:2     # nearest occurrence of c but doesn't match range given below
+\- test:b:1
+   \- test:c:1[1]
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable-with-cycle.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable-with-cycle.txt
new file mode 100644
index 0000000..153f030
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable-with-cycle.txt
@@ -0,0 +1,10 @@
+# Conflict id "a" will be resolved before "x" such that the cycle formed by "x" is still in place when processing "a".
+# So in order to report the conflicting paths to "a" the code better supports cyclic graphs. 
+
+cycle:root:1
++- cycle:x:1       (x)
+|  \- ^x
++- cycle:a:1[1]
+\- cycle:b:1
+   \- cycle:a:2[2]
+      \- cycle:x:1
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable.txt
new file mode 100644
index 0000000..bbc260e
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/unsolvable.txt
@@ -0,0 +1,5 @@
+(null)
++- test:b:1
+|  \- test:a:1[1]
+\- test:c:1
+   \- test:a:2[2]
diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/verbose.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/verbose.txt
new file mode 100644
index 0000000..4bb3880
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/verbose.txt
@@ -0,0 +1,8 @@
+test:root:1
++- test:a:1 test
+|  +- test:x:1 compile
+|  \- test:x:2 compile
+\- test:b:1 test
+   +- test:x:1 compile
+      \- test:z:1 compile
+   \- test:x:2 compile
diff --git a/maven-resolver-util/src/test/resources/visitor/filtering/parents.txt b/maven-resolver-util/src/test/resources/visitor/filtering/parents.txt
new file mode 100644
index 0000000..9a93ca2
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/filtering/parents.txt
@@ -0,0 +1,6 @@
+gid:a:1
++- gid:b:1
+|  \- gid:c:1
+|     \- gid:d:1
+\- gid:e:1
+   \- gid:f:1
diff --git a/maven-resolver-util/src/test/resources/visitor/ordered-list/cycles.txt b/maven-resolver-util/src/test/resources/visitor/ordered-list/cycles.txt
new file mode 100644
index 0000000..8de373c
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/ordered-list/cycles.txt
@@ -0,0 +1,7 @@
+gid:a:1
++- gid:b:1 (1)
+|  \- gid:c:1
+|     \- ^1
+\- gid:d:1
+   +- ^1
+   \- gid:e:1
diff --git a/maven-resolver-util/src/test/resources/visitor/ordered-list/simple.txt b/maven-resolver-util/src/test/resources/visitor/ordered-list/simple.txt
new file mode 100644
index 0000000..094d2d3
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/ordered-list/simple.txt
@@ -0,0 +1,5 @@
+gid:a:1
++- gid:b:1
+|  \- gid:c:1
+\- gid:d:1
+   \- gid:e:1
diff --git a/maven-resolver-util/src/test/resources/visitor/path-recorder/cycle.txt b/maven-resolver-util/src/test/resources/visitor/path-recorder/cycle.txt
new file mode 100644
index 0000000..9a48240
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/path-recorder/cycle.txt
@@ -0,0 +1,7 @@
+gid:a:1 (a)
++- gid:b:1 (b)
+|  \- match:x:1
+\- match:x:2 (x)
+   +- ^a
+   +- ^b
+   \- ^x
diff --git a/maven-resolver-util/src/test/resources/visitor/path-recorder/nested.txt b/maven-resolver-util/src/test/resources/visitor/path-recorder/nested.txt
new file mode 100644
index 0000000..67742f8
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/path-recorder/nested.txt
@@ -0,0 +1,4 @@
+match:x:1
++- gid:a:1
+|  \- match:y:2
+\- match:y:2
diff --git a/maven-resolver-util/src/test/resources/visitor/path-recorder/parents.txt b/maven-resolver-util/src/test/resources/visitor/path-recorder/parents.txt
new file mode 100644
index 0000000..9a93ca2
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/path-recorder/parents.txt
@@ -0,0 +1,6 @@
+gid:a:1
++- gid:b:1
+|  \- gid:c:1
+|     \- gid:d:1
+\- gid:e:1
+   \- gid:f:1
diff --git a/maven-resolver-util/src/test/resources/visitor/path-recorder/simple.txt b/maven-resolver-util/src/test/resources/visitor/path-recorder/simple.txt
new file mode 100644
index 0000000..8f3c6a1
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/path-recorder/simple.txt
@@ -0,0 +1,4 @@
+gid:a:1
++- gid:b:1
+|  \- match:x:1
+\- match:x:2
diff --git a/maven-resolver-util/src/test/resources/visitor/tree/cycles.txt b/maven-resolver-util/src/test/resources/visitor/tree/cycles.txt
new file mode 100644
index 0000000..f8d7abc
--- /dev/null
+++ b/maven-resolver-util/src/test/resources/visitor/tree/cycles.txt
@@ -0,0 +1,6 @@
+gid:a:1
++- gid:b:1 (1)
+|  \- gid:c:1
+|     \- ^1
+\- gid:d:1
+   \- ^1
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..dbf3296
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,447 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven</groupId>
+    <artifactId>maven-parent</artifactId>
+    <version>30</version>
+  </parent>
+
+  <groupId>org.apache.maven.resolver</groupId>
+  <artifactId>maven-resolver</artifactId>
+  <version>1.1.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Maven Artifact Resolver</name>
+  <description>
+    The parent and aggregator for the repository system.
+  </description>
+  <url>https://maven.apache.org/resolver/</url>
+  <inceptionYear>2010</inceptionYear>
+
+  <scm>
+    <connection>scm:git:https://git-wip-us.apache.org/repos/asf/maven-resolver.git</connection>
+    <developerConnection>scm:git:https://git-wip-us.apache.org/repos/asf/maven-resolver.git</developerConnection>
+    <url>https://github.com/apache/maven-resolver/tree/${project.scm.tag}</url>
+    <tag>master</tag>
+  </scm>
+  <issueManagement>
+    <system>jira</system>
+    <url>https://issues.apache.org/jira/browse/MRESOLVER</url>
+  </issueManagement>
+  <ciManagement>
+    <system>Jenkins</system>
+    <url>https://builds.apache.org/job/maven-resolver/</url>
+  </ciManagement>
+  <distributionManagement>
+    <site>
+      <id>apache.website</id>
+      <url>scm:svn:https://svn.apache.org/repos/infra/websites/production/maven/components/${maven.site.path}</url>
+    </site>
+  </distributionManagement>
+
+  <properties>
+    <javaVersion>7</javaVersion>
+    <surefire.redirectTestOutputToFile>true</surefire.redirectTestOutputToFile>
+    <maven.site.path>resolver-archives/resolver-LATEST</maven.site.path>
+    <checkstyle.violation.ignore>UnusedImports,LineLength,InnerAssignment,MagicNumber,AvoidNestedBlocks,ParameterNumber,MethodLength,MemberName</checkstyle.violation.ignore>
+    <sisuVersion>0.3.3</sisuVersion>
+    <slf4jVersion>1.7.25</slf4jVersion>
+  </properties>
+
+  <modules>
+    <!-- NOTE: Be sure to update the bin assembly descriptor as well if the module list changes -->
+    <module>maven-resolver-api</module>
+    <module>maven-resolver-spi</module>
+    <module>maven-resolver-util</module>
+    <module>maven-resolver-impl</module>
+    <module>maven-resolver-test-util</module>
+    <module>maven-resolver-connector-basic</module>
+    <module>maven-resolver-transport-classpath</module>
+    <module>maven-resolver-transport-file</module>
+    <module>maven-resolver-transport-http</module>
+    <module>maven-resolver-transport-wagon</module>
+  </modules>
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-api</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-spi</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-util</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-impl</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-connector-basic</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven.resolver</groupId>
+        <artifactId>maven-resolver-test-util</artifactId>
+        <version>${project.version}</version>
+        <scope>test</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.12</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest-core</artifactId>
+        <version>1.3</version>
+        <scope>test</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>javax.inject</groupId>
+        <artifactId>javax.inject</artifactId>
+        <version>1</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.plexus</groupId>
+        <artifactId>plexus-component-annotations</artifactId>
+        <version>1.7.1</version>
+        <scope>provided</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>org.eclipse.sisu.inject</artifactId>
+        <version>${sisuVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.eclipse.sisu</groupId>
+        <artifactId>org.eclipse.sisu.plexus</artifactId>
+        <version>${sisuVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>javax.enterprise</groupId>
+            <artifactId>cdi-api</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.sonatype.sisu</groupId>
+        <artifactId>sisu-guice</artifactId>
+        <version>3.2.6</version>
+        <classifier>no_aop</classifier>
+        <exclusions>
+          <exclusion>
+            <groupId>aopalliance</groupId>
+            <artifactId>aopalliance</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4jVersion}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <configuration>
+            <detectOfflineLinks>false</detectOfflineLinks>
+            <links>
+              <link>http://download.oracle.com/javase/6/docs/api/</link>
+            </links>
+            <tags>
+              <tag>
+                <name>noextend</name>
+                <placement>a</placement>
+                <head>Restriction:</head>
+              </tag>
+              <tag>
+                <name>noimplement</name>
+                <placement>a</placement>
+                <head>Restriction:</head>
+              </tag>
+              <tag>
+                <name>noinstantiate</name>
+                <placement>a</placement>
+                <head>Restriction:</head>
+              </tag>
+              <tag>
+                <name>nooverride</name>
+                <placement>a</placement>
+                <head>Restriction:</head>
+              </tag>
+              <tag>
+                <name>noreference</name>
+                <placement>a</placement>
+                <head>Restriction:</head>
+              </tag>
+              <tag>
+                <name>provisional</name>
+                <placement>a</placement>
+                <head>Provisional:</head>
+              </tag>
+            </tags>
+            <groups>
+              <group>
+                <title>API</title>
+                <packages>org.eclipse.aether*</packages>
+              </group>
+              <group>
+                <title>SPI</title>
+                <packages>org.eclipse.aether.spi*</packages>
+              </group>
+              <group>
+                <title>Utilities</title>
+                <packages>org.eclipse.aether.util*</packages>
+              </group>
+              <group>
+                <title>Repository Connectors</title>
+                <packages>org.eclipse.aether.connector*</packages>
+              </group>
+              <group>
+                <title>Transporters</title>
+                <packages>org.eclipse.aether.transport*</packages>
+              </group>
+              <group>
+                <title>Implementation</title>
+                <packages>org.eclipse.aether.impl*</packages>
+              </group>
+              <group>
+                <title>Internals</title>
+                <packages>org.eclipse.aether.internal*</packages>
+              </group>
+            </groups>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <configuration>
+            <autoVersionSubmodules>true</autoVersionSubmodules>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>2.20</version>
+          <configuration>
+            <argLine>-Xmx128m</argLine>
+            <redirectTestOutputToFile>${surefire.redirectTestOutputToFile}</redirectTestOutputToFile>
+            <systemPropertyVariables>
+              <java.io.tmpdir>${project.build.directory}/surefire-tmp</java.io.tmpdir>
+            </systemPropertyVariables>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-component-metadata</artifactId>
+          <executions>
+            <execution>
+              <id>generate-components-xml</id>
+              <phase>process-classes</phase>
+              <goals>
+                <goal>generate-metadata</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.eclipse.sisu</groupId>
+          <artifactId>sisu-maven-plugin</artifactId>
+          <version>${sisuVersion}</version>
+          <executions>
+            <execution>
+              <id>generate-index</id>
+              <phase>process-classes</phase>
+              <goals>
+                <goal>main-index</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.rat</groupId>
+          <artifactId>apache-rat-plugin</artifactId>
+          <configuration>
+            <excludes combine.children="append">
+              <exclude>src/test/resources/**/*.ini</exclude>
+              <exclude>src/test/resources/**/*.txt</exclude>
+              <exclude>src/test/resources/ssl/*-store</exclude>
+            </excludes>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <configuration>
+            <archive>
+              <manifestEntries>
+                <Automatic-Module-Name>${AutomaticModuleName}</Automatic-Module-Name>
+              </manifestEntries>
+            </archive>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin><!-- TODO remove when upgrading to parent pom 31 -->
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <dependencies>
+          <dependency>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>extra-enforcer-rules</artifactId>
+            <version>1.0-beta-6</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>clirr</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>clirr-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>check-api-compat</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>check-no-fork</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>reporting</id>
+      <reporting>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <linksource>true</linksource>
+              <notimestamp>true</notimestamp>
+              <quiet>true</quiet>
+            </configuration>
+            <reportSets>
+              <reportSet>
+                <id>aggregate</id>
+                <inherited>false</inherited>
+                <reports>
+                  <report>aggregate</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-jxr-plugin</artifactId>
+            <reportSets>
+              <reportSet>
+                <id>aggregate</id>
+                <inherited>false</inherited>
+                <reports>
+                  <report>aggregate</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+    <profile>
+      <id>m2e</id>
+      <activation>
+        <property>
+          <name>m2e.version</name>
+        </property>
+      </activation>
+      <build>
+        <pluginManagement>
+          <plugins>
+            <plugin>
+              <groupId>org.eclipse.m2e</groupId>
+              <artifactId>lifecycle-mapping</artifactId>
+              <version>1.0.0</version>
+              <configuration>
+                <lifecycleMappingMetadata>
+                  <pluginExecutions>
+                    <pluginExecution>
+                      <pluginExecutionFilter>
+                        <groupId>org.eclipse.sisu</groupId>
+                        <artifactId>sisu-maven-plugin</artifactId>
+                        <versionRange>[${sisuVersion},)</versionRange>
+                        <goals>
+                          <goal>test-index</goal>
+                          <goal>main-index</goal>
+                        </goals>
+                      </pluginExecutionFilter>
+                      <action>
+                        <ignore />
+                      </action>
+                    </pluginExecution>
+                  </pluginExecutions>
+                </lifecycleMappingMetadata>
+              </configuration>
+            </plugin>
+          </plugins>
+        </pluginManagement>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/src/site/resources/download.cgi b/src/site/resources/download.cgi
new file mode 100644
index 0000000..1b178d2
--- /dev/null
+++ b/src/site/resources/download.cgi
@@ -0,0 +1,22 @@
+#!/bin/sh
+#
+# 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.
+#
+# Just call the standard mirrors.cgi script. It will use download.html
+# as the input template.
+exec /www/www.apache.org/dyn/mirrors/mirrors.cgi $*
\ No newline at end of file
diff --git a/src/site/resources/images/maven-resolver-deps.png b/src/site/resources/images/maven-resolver-deps.png
new file mode 100644
index 0000000..955a7ce
--- /dev/null
+++ b/src/site/resources/images/maven-resolver-deps.png
Binary files differ
diff --git a/src/site/site.xml b/src/site/site.xml
new file mode 100644
index 0000000..41387b2
--- /dev/null
+++ b/src/site/site.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.1.0 http://maven.apache.org/xsd/decoration-1.1.0.xsd"
+  name="Artifact Resolver">
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="JavaDocs" href="apidocs/index.html"/>
+      <item name="Source Xref" href="xref/index.html"/>
+      <!--item name="FAQ" href="faq.html"/-->
+      <item name="License" href="http://www.apache.org/licenses/"/>
+      <item name="Download" href="download.html"/>
+    </menu>
+    <menu name="See Also">
+      <item name="Maven Artifact Resolver Demos" href="https://maven.apache.org/resolver-demos/"/>
+      <item name="Maven Artifact Resolver Ant Tasks" href="https://maven.apache.org/resolver-ant-tasks/"/>
+      <item name="Maven Artifact Resolver Provider" href="https://maven.apache.org/ref/current/maven-resolver-provider/"/>
+      <item name="Aether wiki" href="http://wiki.eclipse.org/Aether"/>
+    </menu>
+
+    <menu ref="modules"/>
+    <menu ref="reports"/>
+  </body>
+</project>
\ No newline at end of file
diff --git a/src/site/xdoc/download.xml.vm b/src/site/xdoc/download.xml.vm
new file mode 100644
index 0000000..b37071a
--- /dev/null
+++ b/src/site/xdoc/download.xml.vm
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+
+<!--
+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.
+-->
+
+<document>
+  <properties>
+    <title>Download ${project.name} Source</title>
+  </properties>
+  <body>
+    <section name="Download ${project.name} ${project.version} Source">
+
+      <p>${project.name} ${project.version} is distributed in source format. Use a source archive if you intend to build
+      ${project.name} yourself. Otherwise, simply use the ready-made binary artifacts from central repository.</p>
+
+      <p>You will be prompted for a mirror - if the file is not found on yours, please be patient, as it may take 24
+      hours to reach all mirrors.<p/>
+
+      <p>In order to guard against corrupted downloads/installations, it is highly recommended to
+      <a href="http://www.apache.org/dev/release-signing#verifying-signature">verify the signature</a>
+      of the release bundles against the public <a href="http://www.apache.org/dist/maven/KEYS">KEYS</a> used by the Apache Maven
+      developers.</p>
+
+      <p>${project.name} is distributed under the <a href="http://www.apache.org/licenses/">Apache License, version 2.0</a>.</p>
+
+      <p></p>We <b>strongly</b> encourage our users to configure a Maven repository mirror closer to their location, please read <a href="/guides/mini/guide-mirror-settings.html">How to Use Mirrors for Repositories</a>.</p>
+
+      <a name="mirror"/>
+      <subsection name="Mirror">
+
+        <p>
+          [if-any logo]
+          <a href="[link]">
+            <img align="right" src="[logo]" border="0"
+                 alt="logo"/>
+          </a>
+          [end]
+          The currently selected mirror is
+          <b>[preferred]</b>.
+          If you encounter a problem with this mirror,
+          please select another mirror.
+          If all mirrors are failing, there are
+          <i>backup</i>
+          mirrors
+          (at the end of the mirrors list) that should be available.
+        </p>
+
+        <form action="[location]" method="get" id="SelectMirror">
+          Other mirrors:
+          <select name="Preferred">
+            [if-any http]
+            [for http]
+            <option value="[http]">[http]</option>
+            [end]
+            [end]
+            [if-any ftp]
+            [for ftp]
+            <option value="[ftp]">[ftp]</option>
+            [end]
+            [end]
+            [if-any backup]
+            [for backup]
+            <option value="[backup]">[backup] (backup)</option>
+            [end]
+            [end]
+          </select>
+          <input type="submit" value="Change"/>
+        </form>
+
+        <p>
+          You may also consult the
+          <a href="http://www.apache.org/mirrors/">complete list of
+            mirrors.</a>
+        </p>
+
+      </subsection>
+      
+      <subsection name="${project.name} ${project.version}">
+        
+      <p>This is the current stable version of ${project.name}.</p>
+        
+      <table>
+        <thead>
+          <tr>
+            <th></th>
+            <th>Link</th>
+            <th>Checksum</th>
+            <th>Signature</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>${project.name} ${project.version} (Source zip)</td>
+            <td><a href="[preferred]maven/resolver/${project.artifactId}-${project.version}-source-release.zip">maven/resolver/${project.artifactId}-${project.version}-source-release.zip</a></td>
+            <td><a href="http://www.apache.org/dist/maven/resolver/${project.artifactId}-${project.version}-source-release.zip.md5">maven/resolver/${project.artifactId}-${project.version}-source-release.zip.md5</a></td>
+            <td><a href="http://www.apache.org/dist/maven/resolver/${project.artifactId}-${project.version}-source-release.zip.asc">maven/resolver/${project.artifactId}-${project.version}-source-release.zip.asc</a></td>
+          </tr>
+        </tbody>
+      </table>
+      </subsection>
+
+      <subsection name="Previous Versions">
+        
+      <p>Older non-recommended releases can be found on our <a href="http://archive.apache.org/dist/maven/resolver/">archive site</a>.</p>
+
+      </subsection>
+    </section>
+  </body>
+</document>
+
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
new file mode 100644
index 0000000..9afd662
--- /dev/null
+++ b/src/site/xdoc/index.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+
+<!--
+/*
+ * 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.
+ */
+-->
+
+<document>
+
+  <properties>
+    <title>Introduction</title>
+    <author email="hboutemy_AT_apache_DOT_org">Hervé Boutemy</author>
+  </properties>
+
+  <body>
+
+    <section name="Apache Maven Artifact Resolver">
+
+      <p>Apache Maven Artifact Resolver is a library for working with artifact repositories and dependency resolution.</p>
+      <p>Maven Artifact Resolver deals with the specification of local repository, remote repository, developer workspaces, artifact transports and artifact resolution.</p>
+      <p>It is expected to be extended by concrete repository implementation, like
+        <a href="/ref/current/maven-resolver-provider/">Maven Artifact Resolver Provider</a> for Maven repositories
+        or any other provider for other repository formats.
+      </p>
+
+      <p>
+        <img src="images/maven-resolver-deps.png" width="520" height="265" border="0" usemap="#Maven_Resolver_dependencies" />
+        <map name="Maven_Resolver_dependencies">
+          <area shape="rect" coords="51,229,113,265"  href="./maven-resolver-api/" />
+          <area shape="rect" coords="0,167,62,202"    href="./maven-resolver-spi/" />
+          <area shape="rect" coords="102,166,165,202" href="./maven-resolver-util/" />
+          <area shape="rect" coords="202,166,285,202" href="./maven-resolver-test-util/" />
+          <area shape="rect" coords="0,70,62,106"     href="./maven-resolver-impl/" />
+          <area shape="rect" coords="81,63,187,118"   href="./maven-resolver-connector-basic/" />
+          <area shape="rect" coords="246,32,289,68"   href="./maven-resolver-transport-file/" />
+          <area shape="rect" coords="207,74,310,110"  href="./maven-resolver-transport-classpath/" />
+          <area shape="rect" coords="299,32,382,68"   href="./maven-resolver-transport-wagon/" />
+          <area shape="rect" coords="319,74,382,110"  href="./maven-resolver-transport-http/" />
+          <area shape="rect" coords="414,32,499,68"   href="/wagon/" />
+          <area shape="rect" coords="416,74,519,110"  href="http://hc.apache.org/httpcomponents-client-ga/index.html" />
+        </map>
+      </p>
+
+      <subsection name="See Also">
+        <ul>
+          <li><a href="/resolver-demos/">Maven Artifact Resolver Demos</a></li>
+          <li><a href="/resolver-ant-tasks/">Maven Artifact Resolver Ant Tasks</a></li>
+          <li><a href="/ref/current/maven-resolver-provider/">Maven Artifact Resolver Provider</a></li>
+          <li><a href="http://wiki.eclipse.org/Aether">Aether wiki</a></li>
+        </ul>
+      </subsection>
+    </section>
+
+  </body>
+
+</document>
diff --git a/src/site/xdoc/maven-resolver-deps.odg b/src/site/xdoc/maven-resolver-deps.odg
new file mode 100644
index 0000000..9ffe7d0
--- /dev/null
+++ b/src/site/xdoc/maven-resolver-deps.odg
Binary files differ