From 262d82387e3e792f0f51242b27de86165a5670d5 Mon Sep 17 00:00:00 2001 From: David Turanski Date: Tue, 28 Jul 2015 09:32:23 -0400 Subject: [PATCH] XD-3270 - Implementation of AetherModuleResolver --- spring-cloud-stream-module-launcher/pom.xml | 89 +++++++++++ .../module/resolver/AetherModuleResolver.java | 146 ++++++++++++++++++ .../resolver/AetherModuleResolverTests.java | 108 +++++++++++++ .../src/test/resources/__files/foo.jar | 0 .../foo/bar/foo-bar/1.0.0/foo-bar-1.0.0.jar | 0 5 files changed, 343 insertions(+) create mode 100644 spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolver.java create mode 100644 spring-cloud-stream-module-launcher/src/test/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolverTests.java create mode 100644 spring-cloud-stream-module-launcher/src/test/resources/__files/foo.jar create mode 100644 spring-cloud-stream-module-launcher/src/test/resources/local-repo/foo/bar/foo-bar/1.0.0/foo-bar-1.0.0.jar diff --git a/spring-cloud-stream-module-launcher/pom.xml b/spring-cloud-stream-module-launcher/pom.xml index 4b5cef3b1..4914e5df3 100644 --- a/spring-cloud-stream-module-launcher/pom.xml +++ b/spring-cloud-stream-module-launcher/pom.xml @@ -17,6 +17,9 @@ UTF-8 org.springframework.cloud.stream.module.launcher.ModuleLauncher + 1.0.2.v20150114 + 3.1.0 + 1.57 @@ -28,11 +31,97 @@ org.springframework.boot spring-boot-loader + + org.eclipse.aether + aether-impl + ${aether.version} + + + org.eclipse.aether + aether-connector-basic + ${aether.version} + + + org.eclipse.aether + aether-transport-file + ${aether.version} + + + org.eclipse.aether + aether-transport-http + ${aether.version} + + + org.apache.maven + maven-aether-provider + ${maven.version} + org.springframework.boot spring-boot-starter-test test + + org.springframework.integration + spring-integration-test + ${spring-integration.version} + test + + + ch.qos.logback + logback-classic + test + + + com.github.tomakehurst + wiremock + ${wiremock.version} + test + + standalone + + + org.mortbay.jetty + jetty + + + com.google.guava + guava + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + org.apache.httpcomponents + httpclient + + + org.skyscreamer + jsonassert + + + xmlunit + xmlunit + + + com.jayway.jsonpath + json-path + + + net.sf.jopt-simple + jopt-simple + + + diff --git a/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolver.java b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolver.java new file mode 100644 index 000000000..f35fe6ea6 --- /dev/null +++ b/spring-cloud-stream-module-launcher/src/main/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolver.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.springframework.cloud.stream.module.resolver; + +import java.io.File; +import java.util.LinkedList; +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.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +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.transport.file.FileTransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * An implementation of ModuleResolver using remoteRepositories; + + private final RepositorySystem repositorySystem; + + /** + * Create an instance specifying the locations of the local and remote repositories. + * + * @param localRepository the root path of the local maven repository + * @param remoteRepositories a Map containing pairs of (repository ID,repository URL). This + * may be null or empty if the local repository is off line. + */ + public AetherModuleResolver(File localRepository, Map remoteRepositories) { + Assert.isTrue(localRepository.exists(), "File " + localRepository + " does not exist."); + this.localRepository = localRepository; + this.remoteRepositories = new LinkedList<>(); + if (!CollectionUtils.isEmpty(remoteRepositories)) { + for (Map.Entry remoteRepo : remoteRepositories.entrySet()) { + RemoteRepository remoteRepository = new RemoteRepository.Builder(remoteRepo.getKey(), + DEFAULT_CONTENT_TYPE, remoteRepo.getValue()).build(); + this.remoteRepositories.add(remoteRepository); + } + } + repositorySystem = newRepositorySystem(); + } + + /** + * Resolve an artifact and return its location in the local repository. Aether performs the normal + * Maven resolution process ensuring that the latest update is cached to the local repository. + * @param groupId the groupId + * @param artifactId the artifactId + * @param version the version + * @return a {@ link FileSystemResource} representing the resolved artifact in the local repository. + * @throws a RuntimeException if the artifact does not exist or the resolution fails. + */ + @Override + public Resource resolve(String groupId, String artifactId, String version) { + Artifact artifact = new DefaultArtifact(groupId, artifactId, DEFAULT_CLASSIFIER, DEFAULT_EXTENSION, version); + RepositorySystemSession session = newRepositorySystemSession(repositorySystem, + localRepository.getAbsolutePath()); + ArtifactResult result; + + try { + result = repositorySystem.resolveArtifact(session, new ArtifactRequest(artifact, remoteRepositories, + "runtime")); + } + catch (ArtifactResolutionException e) { + throw new RuntimeException(e); + } + + return new FileSystemResource(result.getArtifact().getFile()); + } + + /* + * Create a session to manage remote and local synchronization. + */ + private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, + String localRepoPath) { + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + LocalRepository localRepo = new LocalRepository(localRepoPath); + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); + return session; + } + + /* + * Aether's components implement {@link org.eclipse.aether.spi.locator.Service} to ease manual wiring. + * Using the prepopulated {@link DefaultServiceLocator}, we need to register the repository connector + * and transporter factories + */ + private RepositorySystem newRepositorySystem() { + + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + locator.addService(TransporterFactory.class, FileTransporterFactory.class); + locator.addService(TransporterFactory.class, HttpTransporterFactory.class); + + + locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() { + @Override + public void serviceCreationFailed(Class type, Class impl, Throwable exception) { + throw new RuntimeException(exception); + } + }); + + return locator.getService(RepositorySystem.class); + } +} diff --git a/spring-cloud-stream-module-launcher/src/test/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolverTests.java b/spring-cloud-stream-module-launcher/src/test/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolverTests.java new file mode 100644 index 000000000..20a2044eb --- /dev/null +++ b/spring-cloud-stream-module-launcher/src/test/java/org/springframework/cloud/stream/module/resolver/AetherModuleResolverTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.springframework.cloud.stream.module.resolver; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.SocketUtils; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author David Turanski + */ +public class AetherModuleResolverTests { + private int port = SocketUtils.findAvailableTcpPort(); + + @Rule + public WireMockRule wireMockRule = new WireMockRule(port); + + @Test + public void testResolveLocal() throws IOException { + ClassPathResource cpr = new ClassPathResource("local-repo"); + File localRepository = cpr.getFile(); + AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, null); + Resource resource = defaultModuleResolver.resolve("foo.bar", "foo-bar", "1.0.0"); + assertTrue(resource.exists()); + assertEquals(resource.getFile().getName(), "foo-bar-1.0.0.jar"); + } + + @Test(expected = RuntimeException.class) + public void testResolveDoesNotExist() throws IOException { + ClassPathResource cpr = new ClassPathResource("local-repo"); + File localRepository = cpr.getFile(); + AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, null); + defaultModuleResolver.resolve("niente", "nada", "zilch"); + } + + @Test + @Ignore + public void testResolveRemote() throws IOException { + ClassPathResource cpr = new ClassPathResource("local-repo"); + File localRepository = cpr.getFile(); + + Map remoteRepos = new HashMap<>(); + + remoteRepos.put("spring", "http://repo.spring.io/release"); + remoteRepos.put("spring-snap", "http://repo.spring.io/snapshot"); + remoteRepos.put("spring-ms", "http://repo.spring.io/milestone"); + + AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos); + Resource resource = defaultModuleResolver.resolve("org.springframework", "spring-core", "4.1.6.RELEASE"); + assertTrue(resource.exists()); + assertEquals(resource.getFile().getName(), "spring-core-4.1.6.RELEASE.jar"); + } + + @Test + public void testResolveMockRemote() throws IOException { + ClassPathResource cpr = new ClassPathResource("local-repo"); + File localRepository = cpr.getFile(); + + ClassPathResource stubJarResource = new ClassPathResource("__files/foo.jar"); + + String stubFileName = stubJarResource.getFile().getName(); + + Map remoteRepos = new HashMap<>(); + + remoteRepos.put("repo0", "http://localhost:" + port + "/repo0"); + remoteRepos.put("repo1", "http://localhost:" + port + "/repo1"); + + stubFor(get(urlEqualTo("/repo1/org/bar/foo/1.0.0/foo-1.0.0.jar")) + .willReturn(aResponse() + .withStatus(200) + .withBodyFile(stubFileName))); + + AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos); + Resource resource = defaultModuleResolver.resolve("org.bar", "foo", "1.0.0"); + assertTrue(resource.exists()); + assertEquals(resource.getFile().getName(), "foo-1.0.0.jar"); + } +} diff --git a/spring-cloud-stream-module-launcher/src/test/resources/__files/foo.jar b/spring-cloud-stream-module-launcher/src/test/resources/__files/foo.jar new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-stream-module-launcher/src/test/resources/local-repo/foo/bar/foo-bar/1.0.0/foo-bar-1.0.0.jar b/spring-cloud-stream-module-launcher/src/test/resources/local-repo/foo/bar/foo-bar/1.0.0/foo-bar-1.0.0.jar new file mode 100644 index 000000000..e69de29bb