XD-3270 - Implementation of AetherModuleResolver

This commit is contained in:
David Turanski
2015-07-28 09:32:23 -04:00
committed by Mark Fisher
parent 219d790910
commit 262d82387e
5 changed files with 343 additions and 0 deletions

View File

@@ -17,6 +17,9 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>org.springframework.cloud.stream.module.launcher.ModuleLauncher</start-class>
<aether.version>1.0.2.v20150114</aether.version>
<maven.version>3.1.0</maven.version>
<wiremock.version>1.57</wiremock.version>
</properties>
<dependencies>
@@ -28,11 +31,97 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-impl</artifactId>
<version>${aether.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-connector-basic</artifactId>
<version>${aether.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-file</artifactId>
<version>${aether.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-http</artifactId>
<version>${aether.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-aether-provider</artifactId>
<version>${maven.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<version>${spring-integration.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
<!-- Include everything below here if you have dependency conflicts -->
<classifier>standalone</classifier>
<exclusions>
<exclusion>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
</exclusion>
<exclusion>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>
</exclusion>
<exclusion>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</exclusion>
<exclusion>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>

View File

@@ -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 <a href="http://www.eclipse.org/aether/>aether</a> resolve the module
* artifact (uber jar) in a local Maven repository, downloading the latest update from a remote repository if
* necessary.
* @author David Turanski
*/
public class AetherModuleResolver implements ModuleResolver {
private static final String DEFAULT_CONTENT_TYPE = "default";
private static final String DEFAULT_CLASSIFIER = "";
private static final String DEFAULT_EXTENSION = "jar";
private final File localRepository;
private final List<RemoteRepository> 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<String, String> remoteRepositories) {
Assert.isTrue(localRepository.exists(), "File " + localRepository + " does not exist.");
this.localRepository = localRepository;
this.remoteRepositories = new LinkedList<>();
if (!CollectionUtils.isEmpty(remoteRepositories)) {
for (Map.Entry<String, String> 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);
}
}

View File

@@ -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<String, String> 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<String, String> 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");
}
}