Add support for resolving plugin versions

This commit adds support for resolving the versions of managed plugins
in a POM.

Closes gh-1438
This commit is contained in:
Stephane Nicoll 2023-07-11 11:40:18 +02:00
parent 8c5c50e196
commit e91f3f3b16
7 changed files with 262 additions and 88 deletions

View File

@ -17,8 +17,8 @@
<java.version>17</java.version>
<commons-compress.version>1.23.0</commons-compress.version>
<commons-text.version>1.10.0</commons-text.version>
<maven.version>3.9.2</maven.version>
<maven-resolver.version>1.9.7</maven-resolver.version>
<maven-resolver-provider.version>3.9.2</maven-resolver-provider.version>
<spring-boot.version>3.1.1</spring-boot.version>
<spring-cloud-contract.version>4.0.2</spring-cloud-contract.version>
</properties>
@ -40,10 +40,15 @@
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>
<version>${maven-resolver-provider.version}</version>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>

View File

@ -21,6 +21,10 @@
</scm>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>

View File

@ -22,14 +22,24 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.model.Model;
import org.apache.maven.model.building.DefaultModelBuilder;
import org.apache.maven.model.building.DefaultModelBuilderFactory;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.resolution.ModelResolver;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectModelResolver;
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.RequestTrace;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.LocalRepositoryManager;
@ -37,6 +47,9 @@ 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.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.spi.locator.ServiceLocator;
@ -44,14 +57,16 @@ 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.
* A {@link MavenVersionResolver} 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
* @author Stephane Nicoll
*/
class MavenResolverDependencyManagementVersionResolver implements DependencyManagementVersionResolver {
@SuppressWarnings("removal")
class DefaultMavenVersionResolver implements MavenVersionResolver, DependencyManagementVersionResolver {
private static final RemoteRepository mavenCentral = new RemoteRepository.Builder("central", "default",
"https://repo1.maven.org/maven2")
@ -72,9 +87,11 @@ class MavenResolverDependencyManagementVersionResolver implements DependencyMana
private final RepositorySystemSession repositorySystemSession;
private final RemoteRepositoryManager remoteRepositoryManager;
private final RepositorySystem repositorySystem;
MavenResolverDependencyManagementVersionResolver(Path cacheLocation) {
DefaultMavenVersionResolver(Path cacheLocation) {
ServiceLocator serviceLocator = createServiceLocator();
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
session.setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy(false, false));
@ -84,10 +101,16 @@ class MavenResolverDependencyManagementVersionResolver implements DependencyMana
session.setUserProperties(System.getProperties());
session.setReadOnly();
this.repositorySystemSession = session;
this.remoteRepositoryManager = serviceLocator.getService(RemoteRepositoryManager.class);
}
@Override
public Map<String, String> resolve(String groupId, String artifactId, String version) {
return resolveDependencies(groupId, artifactId, version);
}
@Override
public Map<String, String> resolveDependencies(String groupId, String artifactId, String version) {
ArtifactDescriptorResult bom = resolveBom(groupId, artifactId, version);
Map<String, String> managedVersions = new HashMap<>();
bom.getManagedDependencies()
@ -98,6 +121,18 @@ class MavenResolverDependencyManagementVersionResolver implements DependencyMana
return managedVersions;
}
@Override
public Map<String, String> resolvePlugins(String groupId, String artifactId, String version) {
Model model = buildEffectiveModel(groupId, artifactId, version);
Map<String, String> managedPluginVersions = new HashMap<>();
model.getBuild()
.getPluginManagement()
.getPlugins()
.forEach((plugin) -> managedPluginVersions.putIfAbsent(plugin.getGroupId() + ":" + plugin.getArtifactId(),
plugin.getVersion()));
return managedPluginVersions;
}
private ArtifactDescriptorResult resolveBom(String groupId, String artifactId, String version) {
synchronized (this.monitor) {
try {
@ -112,6 +147,40 @@ class MavenResolverDependencyManagementVersionResolver implements DependencyMana
}
}
private Model buildEffectiveModel(String groupId, String artifactId, String version) {
try {
ArtifactResult bom = resolvePom(groupId, artifactId, version);
RequestTrace requestTrace = new RequestTrace(null);
ModelResolver modelResolver = new ProjectModelResolver(this.repositorySystemSession, requestTrace,
this.repositorySystem, this.remoteRepositoryManager, repositories,
ProjectBuildingRequest.RepositoryMerging.POM_DOMINANT, null);
DefaultModelBuildingRequest modelBuildingRequest = new DefaultModelBuildingRequest();
modelBuildingRequest.setSystemProperties(System.getProperties());
modelBuildingRequest.setPomFile(bom.getArtifact().getFile());
modelBuildingRequest.setModelResolver(modelResolver);
DefaultModelBuilder modelBuilder = new DefaultModelBuilderFactory().newInstance();
return modelBuilder.build(modelBuildingRequest).getEffectiveModel();
}
catch (ModelBuildingException ex) {
throw new IllegalStateException(
"Model for '" + groupId + ":" + artifactId + ":" + version + "' could not be built", ex);
}
}
private ArtifactResult resolvePom(String groupId, String artifactId, String version) {
synchronized (this.monitor) {
try {
return this.repositorySystem.resolveArtifact(this.repositorySystemSession, new ArtifactRequest(
new DefaultArtifact(groupId, artifactId, "pom", version), repositories, null));
}
catch (ArtifactResolutionException ex) {
throw new IllegalStateException(
"Pom '" + groupId + ":" + artifactId + ":" + version + "' could not be resolved", ex);
}
}
}
private static ServiceLocator createServiceLocator() {
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
locator.addService(RepositorySystem.class, DefaultRepositorySystem.class);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 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.
@ -24,7 +24,9 @@ import java.util.Map;
* managed dependencies of a Maven bom. Implementations must be thread-safe.
*
* @author Andy Wilkinson
* @deprecated as of 0.20.0 in favor of {@link MavenVersionResolver}.
*/
@Deprecated(since = "0.20.0", forRemoval = true)
public interface DependencyManagementVersionResolver {
/**
@ -47,7 +49,7 @@ public interface DependencyManagementVersionResolver {
* @return the resolver
*/
static DependencyManagementVersionResolver withCacheLocation(Path location) {
return new MavenResolverDependencyManagementVersionResolver(location);
return new DefaultMavenVersionResolver(location);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 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 MavenVersionResolver} is used to resolve the versions of managed dependencies
* or plugins. Implementations must be thread-safe.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public interface MavenVersionResolver {
/**
* 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> resolveDependencies(String groupId, String artifactId, String version);
/**
* Resolves the versions in the managed plugins of the pom identified by the given
* {@code groupId}, {@code artifactId}, and {@code version}.
* @param groupId pom group ID
* @param artifactId pom artifact ID
* @param version pom version
* @return the managed plugins as a map of {@code groupId:artifactId} to
* {@code version}
*/
Map<String, String> resolvePlugins(String groupId, String artifactId, String version);
/**
* Creates a new {@code MavenVersionResolver} 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 MavenVersionResolver withCacheLocation(Path location) {
return new DefaultMavenVersionResolver(location);
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2012-2023 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 DefaultMavenVersionResolver}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
class DefaultMavenVersionResolverTests {
private MavenVersionResolver resolver;
@BeforeEach
void createResolver(@TempDir Path temp) {
this.resolver = new DefaultMavenVersionResolver(temp);
}
@Test
void resolveDependenciesForSpringBoot() {
Map<String, String> versions = this.resolver.resolveDependencies("org.springframework.boot",
"spring-boot-dependencies", "2.1.5.RELEASE");
assertThat(versions).containsEntry("org.flywaydb:flyway-core", "5.2.4");
}
@Test
void resolveDependenciesForSpringCloud() {
Map<String, String> versions = this.resolver.resolveDependencies("org.springframework.cloud",
"spring-cloud-dependencies", "Greenwich.SR1");
assertThat(versions).containsEntry("com.netflix.ribbon:ribbon", "2.3.0");
}
@Test
void resolveDependenciesUsingMilestones() {
Map<String, String> versions = this.resolver.resolveDependencies("org.springframework.boot",
"spring-boot-dependencies", "2.2.0.M3");
assertThat(versions).containsEntry("org.flywaydb:flyway-core", "5.2.4");
}
@Test
void resolveDependenciesUsingSnapshots() {
Map<String, String> versions = this.resolver.resolveDependencies("org.springframework.boot",
"spring-boot-dependencies", "2.4.0-SNAPSHOT");
assertThat(versions).isNotEmpty();
}
@Test
void resolveDependenciesForNonExistentDependency() {
assertThatIllegalStateException()
.isThrownBy(() -> this.resolver.resolveDependencies("org.springframework.boot", "spring-boot-bom", "1.0"))
.withMessage("Bom 'org.springframework.boot:spring-boot-bom:1.0' could not be resolved");
}
@Test
void resolvePluginsForSpringBoot() {
Map<String, String> versions = this.resolver.resolvePlugins("org.springframework.boot",
"spring-boot-starter-parent", "3.1.1");
assertThat(versions).containsEntry("org.springframework.boot:spring-boot-maven-plugin", "3.1.1");
}
@Test
void resolvePluginsUsingMilestones() {
Map<String, String> versions = this.resolver.resolvePlugins("org.springframework.boot",
"spring-boot-dependencies", "2.2.0.M3");
assertThat(versions).containsEntry("org.springframework.boot:spring-boot-maven-plugin", "2.2.0.M3");
}
@Test
void resolvePluginsUsingSnapshots() {
Map<String, String> versions = this.resolver.resolvePlugins("org.springframework.boot",
"spring-boot-dependencies", "2.4.0-SNAPSHOT");
assertThat(versions).isNotEmpty();
}
@Test
void resolvePluginsForNonExistentDependency() {
assertThatIllegalStateException()
.isThrownBy(() -> this.resolver.resolvePlugins("org.springframework.boot", "spring-boot-bom", "1.0"))
.withMessage("Pom 'org.springframework.boot:spring-boot-bom:1.0' could not be resolved");
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright 2012-2023 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.4.0-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");
}
}