Provide support for resolving versions from Maven boms

This commit introduces a new module, initializer-version-resolver,
that provides support for resolving versions from the dependency
management of a Maven bom. Providing with the bom's coordinates
(groupId, artifactId, and version) a map of groupId:artifactId to
version, derived from the bom's <dependencyManagement>, is returned.

Closes gh-934
This commit is contained in:
Andy Wilkinson 2019-06-21 15:51:43 +01:00
parent 1c7a73c29f
commit ab8b1158fa
6 changed files with 327 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>io.spring.initializr</groupId>
<artifactId>initializr</artifactId>
<version>${revision}</version>
</parent>
<artifactId>initializr-version-resolver</artifactId>
<name>Spring Initializr :: Version Resolver</name>
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-connector-basic</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-http</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.versionresolver;
import java.nio.file.Path;
import java.util.Map;
/**
* A {@code DependencyManagementVersionResolver} is used to resolve the versions in the
* managed dependencies of a Maven bom. Implementations must be thread-safe.
*
* @author Andy Wilkinson
*/
public interface DependencyManagementVersionResolver {
/**
* Resolves the versions in the managed dependencies of the bom identified by the
* given {@code groupId}, {@code artifactId}, and {@code version}.
* @param groupId bom group ID
* @param artifactId bom artifact ID
* @param version bom version
* @return the managed dependencies as a map of {@code groupId:artifactId} to
* {@code version}
*/
Map<String, String> resolve(String groupId, String artifactId, String version);
/**
* Creates a new {@code DependencyManagementVersionResolver} that uses the given
* {@code location} for its local cache. To avoid multiple instances attempting to
* write to the same location cache, callers should ensure that a unique location is
* used. The returned resolver can then be used concurrently by multiple threads.
* @param location cache location
* @return the resolver
*/
static DependencyManagementVersionResolver withCacheLocation(Path location) {
return new MavenResolverDependencyManagementVersionResolver(location);
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.versionresolver;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
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.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.locator.ServiceLocator;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy;
/**
* A {@link DependencyManagementVersionResolver} that resolves versions using Maven
* Resolver. Maven's default {@link LocalRepositoryManager} implementation is not
* thread-safe. To avoid corruption of the local repository, interaction with the
* {@link RepositorySystem} is single-threaded.
*
* @author Andy Wilkinson
*/
class MavenResolverDependencyManagementVersionResolver implements DependencyManagementVersionResolver {
private static final RemoteRepository mavenCentral = new RemoteRepository.Builder("central", "default",
"https://repo1.maven.org/maven2").build();
private static final RemoteRepository springMilestones = new RemoteRepository.Builder("spring-milestones",
"default", "https://repo.spring.io/milestone").build();
private static final RemoteRepository springSnapshots = new RemoteRepository.Builder("spring-snapshots", "default",
"https://repo.spring.io/snapshot").build();
private static final List<RemoteRepository> repositories = Arrays.asList(mavenCentral, springMilestones,
springSnapshots);
private final Object monitor = new Object();
private final RepositorySystemSession repositorySystemSession;
private final RepositorySystem repositorySystem;
MavenResolverDependencyManagementVersionResolver(Path cacheLocation) {
ServiceLocator serviceLocator = createServiceLocator();
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
session.setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy(false, false));
LocalRepository localRepository = new LocalRepository(cacheLocation.toFile());
this.repositorySystem = serviceLocator.getService(RepositorySystem.class);
session.setLocalRepositoryManager(this.repositorySystem.newLocalRepositoryManager(session, localRepository));
session.setReadOnly();
this.repositorySystemSession = session;
}
@Override
public Map<String, String> resolve(String groupId, String artifactId, String version) {
ArtifactDescriptorResult bom = resolveBom(groupId, artifactId, version);
Map<String, String> managedVersions = new HashMap<>();
bom.getManagedDependencies().stream().map((dependency) -> dependency.getArtifact())
.forEach((artifact) -> managedVersions
.putIfAbsent(artifact.getGroupId() + ":" + artifact.getArtifactId(), artifact.getVersion()));
return managedVersions;
}
private ArtifactDescriptorResult resolveBom(String groupId, String artifactId, String version) {
synchronized (this.monitor) {
try {
return this.repositorySystem.readArtifactDescriptor(this.repositorySystemSession,
new ArtifactDescriptorRequest(new DefaultArtifact(groupId, artifactId, "pom", version),
repositories, null));
}
catch (ArtifactDescriptorException ex) {
throw new IllegalStateException(
"Bom '" + groupId + ":" + artifactId + ":" + version + "' could not be resolved", ex);
}
}
}
private static ServiceLocator createServiceLocator() {
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
locator.addService(RepositorySystem.class, DefaultRepositorySystem.class);
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
return locator;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Classes relating to version resolution.
*/
package io.spring.initializr.versionresolver;

View File

@ -0,0 +1,78 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.versionresolver;
import java.nio.file.Path;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link MavenResolverDependencyManagementVersionResolver}.
*
* @author Andy Wilkinson
*/
class MavenResolverDependencyManagementVersionResolverTests {
private DependencyManagementVersionResolver resolver;
@BeforeEach
void createResolver(@TempDir Path temp) {
this.resolver = new MavenResolverDependencyManagementVersionResolver(temp);
}
@Test
void springBootDependencies() {
Map<String, String> versions = this.resolver.resolve("org.springframework.boot", "spring-boot-dependencies",
"2.1.5.RELEASE");
assertThat(versions).containsEntry("org.flywaydb:flyway-core", "5.2.4");
}
@Test
void springCloudDependencies() {
Map<String, String> versions = this.resolver.resolve("org.springframework.cloud", "spring-cloud-dependencies",
"Greenwich.SR1");
assertThat(versions).containsEntry("com.netflix.ribbon:ribbon", "2.3.0");
}
@Test
void milestoneBomCanBeResolved() {
Map<String, String> versions = this.resolver.resolve("org.springframework.boot", "spring-boot-dependencies",
"2.2.0.M3");
assertThat(versions).containsEntry("org.flywaydb:flyway-core", "5.2.4");
}
@Test
void snapshotBomCanBeResolved() {
Map<String, String> versions = this.resolver.resolve("org.springframework.boot", "spring-boot-dependencies",
"2.2.0.BUILD-SNAPSHOT");
assertThat(versions).isNotEmpty();
}
@Test
void nonExistentDependency() {
assertThatIllegalStateException()
.isThrownBy(() -> this.resolver.resolve("org.springframework.boot", "spring-boot-bom", "1.0"))
.withMessage("Bom 'org.springframework.boot:spring-boot-bom:1.0' could not be resolved");
}
}

18
pom.xml
View File

@ -42,6 +42,8 @@
<main.basedir>${basedir}</main.basedir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit-jupiter.version>5.4.2</junit-jupiter.version>
<maven.version>3.6.1</maven.version>
<maven-resolver.version>1.3.3</maven-resolver.version>
<spring-boot.version>2.1.6.RELEASE</spring-boot.version>
<spring-cloud-contract.version>2.1.1.RELEASE</spring-cloud-contract.version>
<spring-javaformat.version>0.0.12</spring-javaformat.version>
@ -54,6 +56,7 @@
<module>initializr-generator-spring</module>
<module>initializr-metadata</module>
<module>initializr-service-sample</module>
<module>initializr-version-resolver</module>
<module>initializr-web</module>
</modules>
@ -102,6 +105,21 @@
<version>${revision}</version>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-connector-basic</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-http</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>