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 -> b -> c -> 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>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("<" 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( "foo", "bar" );
+ * String def = "gid:%s:ext:ver\n" + "+- gid:%s:ext:ver";
+ * </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" <
+ * "beta" = "b" < "milestone" = "m" < "cr" = "rc" < "snapshot" < "final" = "ga" < "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" < "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