diff --git a/initializr-version-resolver/pom.xml b/initializr-version-resolver/pom.xml new file mode 100644 index 00000000..d8ed68b2 --- /dev/null +++ b/initializr-version-resolver/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + io.spring.initializr + initializr + ${revision} + + initializr-version-resolver + Spring Initializr :: Version Resolver + + + ${basedir}/.. + + + + + org.apache.maven + maven-resolver-provider + + + org.apache.maven.resolver + maven-resolver-connector-basic + + + org.apache.maven.resolver + maven-resolver-transport-http + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + diff --git a/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/DependencyManagementVersionResolver.java b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/DependencyManagementVersionResolver.java new file mode 100644 index 00000000..f531a34e --- /dev/null +++ b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/DependencyManagementVersionResolver.java @@ -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 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); + } + +} diff --git a/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolver.java b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolver.java new file mode 100644 index 00000000..9d45d4a9 --- /dev/null +++ b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolver.java @@ -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 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 resolve(String groupId, String artifactId, String version) { + ArtifactDescriptorResult bom = resolveBom(groupId, artifactId, version); + Map 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; + } + +} diff --git a/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/package-info.java b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/package-info.java new file mode 100644 index 00000000..4ecab6c7 --- /dev/null +++ b/initializr-version-resolver/src/main/java/io/spring/initializr/versionresolver/package-info.java @@ -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; diff --git a/initializr-version-resolver/src/test/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolverTests.java b/initializr-version-resolver/src/test/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolverTests.java new file mode 100644 index 00000000..fa42c486 --- /dev/null +++ b/initializr-version-resolver/src/test/java/io/spring/initializr/versionresolver/MavenResolverDependencyManagementVersionResolverTests.java @@ -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 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 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 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 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"); + } + +} diff --git a/pom.xml b/pom.xml index 50819f49..854ea174 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,8 @@ ${basedir} UTF-8 5.4.2 + 3.6.1 + 1.3.3 2.1.6.RELEASE 2.1.1.RELEASE 0.0.12 @@ -54,6 +56,7 @@ initializr-generator-spring initializr-metadata initializr-service-sample + initializr-version-resolver initializr-web @@ -102,6 +105,21 @@ ${revision} test-jar + + org.apache.maven + maven-resolver-provider + ${maven.version} + + + org.apache.maven.resolver + maven-resolver-connector-basic + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-http + ${maven-resolver.version} + org.junit junit-bom