Move module launcher/configuration metadata

This resolves #320
This commit is contained in:
Ilayaperumal Gopinathan
2016-02-25 10:20:51 +05:30
committed by Marius Bogoevici
parent 4dfd217db6
commit 00a5ac4341
36 changed files with 0 additions and 2384 deletions

View File

@@ -19,7 +19,6 @@
</scm>
<properties>
<java.version>1.7</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
@@ -40,11 +39,9 @@
<module>spring-cloud-stream-tuple</module>
<module>spring-cloud-stream-starters</module>
<module>spring-cloud-stream-rxjava</module>
<module>spring-cloud-stream-module-launcher</module>
<module>spring-cloud-stream-test-support</module>
<module>spring-cloud-stream-test-support-internal</module>
<module>spring-cloud-stream-integration-tests</module>
<module>spring-cloud-stream-configuration-metadata</module>
<module>spring-cloud-stream-docs</module>
</modules>
<build>

View File

@@ -1,21 +0,0 @@
<?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>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-stream-configuration-metadata</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-module-launcher</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-metadata</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,157 +0,0 @@
/*
* 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.configuration.metadata;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataGroup;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.jar.JarFile;
import org.springframework.cloud.stream.module.launcher.ModuleJarLauncher;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* Used to retrieve metadata about the configuration properties that can alter a module behavior.
*
* @author Eric Bottard
*/
public class ModuleConfigurationMetadataResolver {
public ModuleConfigurationMetadataResolver() {
JarFile.registerUrlProtocolHandler();
}
/**
* Return metadata about configuration properties (as groups) that are documented via
* <a href="http://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html">
* Spring Boot configuration metadata</a> and visible in a module.
*
* @param module a Spring Cloud Stream module; typically a Boot uberjar,
* but directories are supported as well
*/
public List<ConfigurationMetadataGroup> listPropertyGroups(Resource module) {
List<ConfigurationMetadataGroup> result = new ArrayList<>();
ClassLoader classLoader = null;
try {
File moduleFile = module.getFile();
Archive archive = moduleFile.isDirectory() ? new ExplodedArchive(moduleFile) : new JarFileArchive(moduleFile);
classLoader = createClassLoader(archive);
ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
ResourcePatternResolver moduleResourceLoader = new PathMatchingResourcePatternResolver(classLoader);
for (Resource r : moduleResourceLoader.getResources("classpath*:/META-INF/*spring-configuration-metadata.json")) {
builder.withJsonResource(r.getInputStream());
}
for (ConfigurationMetadataGroup group : builder.build().getAllGroups().values()) {
result.add(group);
}
}
catch (Exception e) {
throw new RuntimeException("Exception trying to list configuration properties for module " + module, e);
}
finally {
if (classLoader instanceof Closeable) {
try {
((Closeable) classLoader).close();
}
catch (IOException e) {
// ignore
}
}
}
return result;
}
/**
* Return metadata about configuration properties that are documented via
* <a href="http://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html">
* Spring Boot configuration metadata</a> and visible in a module.
*
* @param module a Spring Cloud Stream module; typically a Boot uberjar,
* but directories are supported as well
*/
public List<ConfigurationMetadataProperty> listProperties(Resource module) {
List<ConfigurationMetadataProperty> result = new ArrayList<>();
ClassLoader classLoader = null;
try {
File moduleFile = module.getFile();
Archive archive = moduleFile.isDirectory() ? new ExplodedArchive(moduleFile) : new JarFileArchive(moduleFile);
classLoader = createClassLoader(archive);
ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
ResourcePatternResolver moduleResourceLoader = new PathMatchingResourcePatternResolver(classLoader);
for (Resource r : moduleResourceLoader.getResources("classpath*:/META-INF/*spring-configuration-metadata.json")) {
builder.withJsonResource(r.getInputStream());
}
for (ConfigurationMetadataProperty property : builder.build().getAllProperties().values()) {
result.add(property);
}
}
catch (Exception e) {
throw new RuntimeException("Exception trying to list configuration properties for module " + module, e);
}
finally {
if (classLoader instanceof Closeable) {
try {
((Closeable) classLoader).close();
}
catch (IOException e) {
// ignore
}
}
}
return result;
}
/**
* Return a {@link ClassLoader} for accessing resources in the provided
* {@link Archive}. The caller is responsible for disposing of the
* class loader.
*
* @param archive the archive for which to return a class loader
* @return class loader for the given archive
* @throws Exception if the class loader cannot be created
*/
protected ClassLoader createClassLoader(Archive archive) throws Exception {
return new ClassLoaderExposingJarLauncher(archive).createClassLoader();
}
/**
* Extension of {@link ModuleJarLauncher} used for exposing a {@link ClassLoader}
* for the provided {@link Archive}.
*/
private static class ClassLoaderExposingJarLauncher extends ModuleJarLauncher {
public ClassLoaderExposingJarLauncher(Archive archive) {
super(archive);
}
protected ClassLoader createClassLoader() throws Exception {
List<Archive> classPathArchives = getClassPathArchives();
return createClassLoader(classPathArchives);
}
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.configuration.metadata;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
/**
* Automatically exposes a ModuleConfigurationMetadataResolver if none is already registered.
*
* @author Eric Bottard
*/
@Configuration
public class ModuleConfigurationMetadataResolverAutoConfiguration {
@Bean
@ConditionalOnMissingBean(ModuleConfigurationMetadataResolver.class)
public ModuleConfigurationMetadataResolver metadataResolver() {
return new ModuleConfigurationMetadataResolver();
}
}

View File

@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration:\
org.springframework.cloud.stream.configuration.metadata.ModuleConfigurationMetadataResolverAutoConfiguration

View File

@@ -73,11 +73,6 @@
<artifactId>spring-cloud-stream-binder-test</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-module-launcher</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-tuple</artifactId>

View File

@@ -1,88 +0,0 @@
# Module Launcher
The Module Launcher provides a single entry point that bootstraps module JARs located in a Maven repository. A single Docker image can then be used to launch any of those JARs based on an environment variable. When running standalone, a system property may be used instead of an environment variable, so that multiple instances of the Module Launcher may run on a single machine. The following examples demonstrate running the modules for the *ticktock* stream (`time-source | log-sink`).
## Prerequisites
1: clone and build the spring-cloud-stream project:
````
git clone https://github.com/spring-cloud/spring-cloud-stream.git
cd spring-cloud-stream
mvn package
cd ..
````
2: start redis locally via `redis-server` or `docker-compose` (there's a `docker-compose.yml` in `spring-cloud-stream-samples`). Optionally start `redis-cli` and use the `MONITOR` command to watch activity.
*NOTE:* redis.conf (on OSX it is found here: /usr/local/etc/redis.conf) may need to be updated to set the binding to an address other than 127.0.0.1 else the docker instances will fail to connect. For example: bind 0.0.0.0
## Running Standalone
From the `spring-cloud-stream/spring-cloud-stream-module-launcher` directory:
````
java -Dmodules=org.springframework.cloud.stream.module:time-source:jar:exec:1.0.0.BUILD-SNAPSHOT -Dspring.cloud.stream.bindings.output=ticktock -jar target/spring-cloud-stream-module-launcher-1.0.0.BUILD-SNAPSHOT.jar
java -Dmodules=org.springframework.cloud.stream.module:log-sink:jar:exec:1.0.0.BUILD-SNAPSHOT -Dargs.0.server.port=8081 -Dspring.cloud.stream.bindings.input=ticktock -jar target/spring-cloud-stream-module-launcher-1.0.0.BUILD-SNAPSHOT.jar
````
Note that `server.port` needs to be specified explicitly for the log sink module as the time source module already uses the default port `8080`.
The module launcher is able to launch several modules, hence the `args.0.` prefix.
The binding property is set to use the same name `ticktock` for both the output/input bindings of source/sink modules so that the log sink receives messages from the time source.
The time messages will be emitted every second. The console for the log module will display each:
````
2015-08-26 14:21:44.546 INFO 35725 --- [hannel-adapter1] o.s.cloud.stream.module.log.LogSink : Received: 2015-08-26 14:21:44
2015-08-26 14:21:45.548 INFO 35725 --- [hannel-adapter1] o.s.cloud.stream.module.log.LogSink : Received: 2015-08-26 14:21:45
2015-08-26 14:21:46.550 INFO 35725 --- [hannel-adapter1] o.s.cloud.stream.module.log.LogSink : Received: 2015-08-26 14:21:46
````
*NOTE:* the two modules will be launched within a single process if both are provided (comma-delimited) via `-Dmodules`
## Running with Docker
The easiest way to get a demo working is to use `docker-compose` (From the `spring-cloud-stream/spring-cloud-stream-module-launcher` directory):
Make sure to set `DOCKER_HOST`. If you are running a `boot2docker` VM, $(boot2docker shellinit) would set that up.
```
$ mvn package docker:build
$ docker-compose up
...
logsink_1 | 2015-08-11 08:25:49.909 INFO 1 --- [hannel-adapter1] o.s.cloud.stream.module.log.LogSink : Received: 2015-08-11 08:25:49
logsink_1 | 2015-08-11 08:25:50.909 INFO 1 --- [hannel-adapter1] o.s.cloud.stream.module.log.LogSink : Received: 2015-08-11 08:25:50
...
```
You can also run each module individually as a Docker process by passing environment variables for the module name as well as the host machine's IP address for the redis connection to be established within the container:
To find out redis host IP:
```
Get the container ID of redis: `docker ps`
Get the IP address by inspecting the container: `docker inspect <containerID>`
```
To run the modules individually on docker:
````
docker run -p 8080:8080 -e MODULES=org.springframework.cloud.stream.module:time-source:jar:exec:1.0.0.BUILD-SNAPSHOT \
-e spring.cloud.stream.bindings.output=ticktock -e SPRING_REDIS_HOST=<Redis-Host-IP> springcloud/stream-module-launcher
docker run -p 8081:8080 -e MODULES=org.springframework.cloud.stream.module:log-sink:jar:exec:1.0.0.BUILD-SNAPSHOT \
-e spring.cloud.stream.bindings.input=ticktock -e SPRING_REDIS_HOST=<Redis-Host-IP> springcloud/stream-module-launcher
````
Note the binding name `ticktock` is specified for the source's output and sink's input.
The port mapping is done so that individual modules' http endpoints can be accessed via the docker VM port.
To run pub/sub modules individually on docker, the binding name has to start with `topic:`.
````
docker run -p 8080:8080 -e MODULES=org.springframework.cloud.stream.module:time-source:jar:exec:1.0.0.BUILD-SNAPSHOT \
-e spring.cloud.stream.bindings.output=topic:foo -e SPRING_REDIS_HOST=<Redis-Host-IP> springcloud/stream-module-launcher
docker run -p 8081:8080 -e MODULES=org.springframework.cloud.stream.module:log-sink:jar:exec:1.0.0.BUILD-SNAPSHOT \
-e spring.cloud.stream.bindings.input=topic:foo -e SPRING_REDIS_HOST=<Redis-Host-IP> springcloud/stream-module-launcher
docker run -p 8082:8080 -e MODULES=org.springframework.cloud.stream.module:log-sink:jar:exec:1.0.0.BUILD-SNAPSHOT \
-e spring.cloud.stream.bindings.input=topic:foo -e SPRING_REDIS_HOST=<Redis-Host-IP> springcloud/stream-module-launcher
````
In the above scenario, both the sink modules receive the same messages from the time source.

View File

@@ -1,24 +0,0 @@
redis:
image: redis
timesource:
image: springcloud/stream-module-launcher
links:
- redis
ports:
- "8080:8080"
environment:
SPRING_REDIS_HOST: redis
MODULES: org.springframework.cloud.stream.module:time-source:1.0.0.BUILD-SNAPSHOT
SPRING_CLOUD_STREAM_BINDINGS_OUTPUT: ticktock
logsink:
image: springcloud/stream-module-launcher
links:
- redis
ports:
- "8081:8080"
environment:
SPRING_REDIS_HOST: redis
MODULES: org.springframework.cloud.stream.module:log-sink:1.0.0.BUILD-SNAPSHOT
SPRING_CLOUD_STREAM_BINDINGS_INPUT: ticktock

View File

@@ -1,154 +0,0 @@
<?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>
<artifactId>spring-cloud-stream-module-launcher</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-stream-module-launcher</name>
<description>application for launching spring-cloud-stream modules</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<start-class>org.springframework.cloud.stream.module.launcher.ModuleLauncherApplication</start-class>
<aether.version>1.0.2.v20150114</aether.version>
<maven.version>3.1.0</maven.version>
<wiremock.version>1.57</wiremock.version>
<docker.image.prefix>springcloud</docker.image.prefix>
<docker.image.name>stream-module-launcher</docker.image.name>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<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>
<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>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.2.3</version>
<configuration>
<imageName>${docker.image.prefix}/${docker.image.name}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,5 +0,0 @@
FROM anapsix/alpine-java:8
VOLUME /tmp
ADD *.jar module-launcher.jar
RUN bash -c 'touch /module-launcher.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/module-launcher.jar"]

View File

@@ -1,39 +0,0 @@
/*
* 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.launcher;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.util.AsciiBytes;
/**
* @author Marius Bogoevici
*/
class ArchiveMatchingEntryFilter implements Archive.EntryFilter {
public static final ArchiveMatchingEntryFilter FILTER = new ArchiveMatchingEntryFilter();
private static final AsciiBytes LIB = new AsciiBytes("lib/");
@Override
public boolean matches(Archive.Entry entry) {
return isNestedArchive(entry);
}
protected boolean isNestedArchive(Archive.Entry entry) {
return !entry.isDirectory() && entry.getName().startsWith(LIB);
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.launcher;
import java.lang.reflect.Method;
import java.net.URL;
import org.springframework.boot.loader.JarLauncher;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.cloud.stream.module.utils.ClassloaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* A {@link JarLauncher} that supports launching module archive.
*
* Also, it restricts the classloader of the launched application to the contents
* of the uber-jar and the extension classloader of the JVM.
*
* @author Mark Fisher
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
public class ModuleJarLauncher extends JarLauncher {
public ModuleJarLauncher(Archive archive) {
super(archive);
}
@Override
public void launch(String[] args) {
super.launch(args);
}
@Override
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
if (ClassUtils.isPresent(
"org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
classLoader)) {
// Ensure the method is invoked on a class that is loaded by the provided
// class loader (not the current context class loader):
Method method = ReflectionUtils
.findMethod(classLoader.loadClass("org.apache.catalina.webresources.TomcatURLStreamHandlerFactory"),
"disable");
ReflectionUtils.invokeMethod(method, null);
}
super.launch(args, mainClass, classLoader);
}
@Override
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return ClassloaderUtils.createModuleClassloader(urls);
}
}

View File

@@ -1,55 +0,0 @@
/*
* 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.launcher;
import java.util.HashMap;
import java.util.Map;
/**
* Encapsulates a reference to a module (as maven coordinates) and a set of "arguments" that must be passed to it.
* Those arguments will be passed to the module by the launcher.
*
* @author Eric Bottard
*/
public class ModuleLaunchRequest {
private final String module;
private final Map<String, String> arguments;
public ModuleLaunchRequest(String module, Map<String, String> arguments) {
this.module = module;
this.arguments = arguments != null ? new HashMap<>(arguments) : new HashMap<String, String>();
}
public String getModule() {
return module;
}
public Map<String, String> getArguments() {
return arguments;
}
public void addArgument(String name, String value) {
this.arguments.put(name, value);
}
@Override
public String toString() {
return String.format("%s with arguments %s", module, arguments);
}
}

View File

@@ -1,321 +0,0 @@
/*
* 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.launcher;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.cloud.stream.module.resolver.Coordinates;
import org.springframework.cloud.stream.module.resolver.ModuleResolver;
import org.springframework.cloud.stream.module.utils.ClassloaderUtils;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* A component that launches one or more modules, delegating their resolution to an
* underlying {@link ModuleResolver}.
*
* @author Mark Fisher
* @author Ilayaperumal Gopinathan
* @author Marius Bogoevici
* @author Eric Bottard
*/
public class ModuleLauncher {
public static final String AGGREGATE_APPLICATION_CLASS = "org.springframework.cloud.stream.aggregate.AggregateApplication";
public static final String AGGREGATE_APPLICATION_RUN_METHOD = "run";
public static final String MODULE_AGGREGATOR_RUNNER_THREAD_NAME = "module-aggregator-runner";
private static Log log = LogFactory.getLog(ModuleLauncher.class);
private static final String DEFAULT_EXTENSION = "jar";
private static final String DEFAULT_CLASSIFIER = "";
private static final Pattern COORDINATES_PATTERN =
Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)");
private static final String INCLUDE_DEPENDENCIES_ARG = "includes";
private static final String EXCLUDE_DEPENDENCIES_ARG = "excludes";
private final ModuleResolver moduleResolver;
/**
* Creates a module launcher using the provided module resolver
* @param moduleResolver the module resolver instance to use
*/
public ModuleLauncher(ModuleResolver moduleResolver) {
this.moduleResolver = moduleResolver;
}
/**
* Launches one or more modules, with the corresponding arguments, if any.
*
* Modules must be passed in "natural" left to right order.
*
* The format of each module must conform to the <a href="http://www.eclipse.org/aether">Aether</a> convention:
* <code>&lt;groupId&gt;:&lt;artifactId&gt;[:&lt;extension&gt;[:&lt;classifier&gt;]]:&lt;version&gt;</code>
*
* @param moduleLaunchRequests a list of modules with their arguments
* @param aggregate whether the modules should be aggregated at launch
* @param aggregateArgs a list of arguments for the whole aggregate
*/
@SuppressWarnings("unchecked")
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests, boolean aggregate, Map<String,String> aggregateArgs) {
if (moduleLaunchRequests.size() == 1 || !aggregate) {
launchIndividualModules(moduleLaunchRequests);
}
else {
launchAggregatedModules(moduleLaunchRequests, aggregateArgs != null ? aggregateArgs : Collections.EMPTY_MAP);
}
}
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests, boolean aggregate) {
this.launch(moduleLaunchRequests, aggregate, Collections.<String,String>emptyMap());
}
public void launch(List<ModuleLaunchRequest> moduleLaunchRequests) {
this.launch(moduleLaunchRequests, false);
}
/**
* Converts a set of semantic program arguments to "command line program arguments" that is, to the
* {@literal --foo=bar} form.
*/
private String[] toArgArray(Map<String, String> args) {
if (args != null) {
String[] result = new String[args.size()];
int i = 0;
for (Map.Entry<String, String> kv : args.entrySet()) {
result[i++] = String.format("--%s=%s", kv.getKey(), kv.getValue());
}
return result;
}
else {
return new String[0];
}
}
public void launchAggregatedModules(List<ModuleLaunchRequest> moduleLaunchRequests, Map<String,String> aggregateArgs) {
try {
List<String> mainClassNames = new ArrayList<>();
LinkedHashSet<URL> jarURLs = new LinkedHashSet<>();
List<String> seenArchives = new ArrayList<>();
final List<String[]> arguments = new ArrayList<>();
final ClassLoader classLoader;
if (!(aggregateArgs.containsKey(EXCLUDE_DEPENDENCIES_ARG) ||
aggregateArgs.containsKey(INCLUDE_DEPENDENCIES_ARG))) {
for (ModuleLaunchRequest moduleLaunchRequest : moduleLaunchRequests) {
Resource resource = resolveModule(moduleLaunchRequest.getModule());
JarFileArchive jarFileArchive = new JarFileArchive(resource.getFile());
jarURLs.add(jarFileArchive.getUrl());
for (Archive archive : jarFileArchive.getNestedArchives(ArchiveMatchingEntryFilter.FILTER)) {
// avoid duplication based on unique JAR names
String urlAsString = archive.getUrl().toString();
String jarNameWithExtension = urlAsString.substring(0, urlAsString.lastIndexOf("!/"));
String jarNameWithoutExtension =
jarNameWithExtension.substring(jarNameWithExtension.lastIndexOf("/") + 1);
if (!seenArchives.contains(jarNameWithoutExtension)) {
seenArchives.add(jarNameWithoutExtension);
jarURLs.add(archive.getUrl());
}
}
mainClassNames.add(jarFileArchive.getMainClass());
arguments.add(toArgArray(moduleLaunchRequest.getArguments()));
}
classLoader = ClassloaderUtils.createModuleClassloader(jarURLs.toArray(new URL[jarURLs.size()]));
} else {
// First, resolve modules and extract main classes - while slightly less efficient than just
// doing the same processing after resolution, this ensures that module artifacts are processed
// correctly for extracting their main class names. It is not possible in the general case to
// identify, after resolution, whether a resource represents a module artifact which was part of the
// original request or not. We will include the first module as root and the next as direct dependencies
Coordinates root = null;
ArrayList<Coordinates> includeCoordinates = new ArrayList<>();
for (ModuleLaunchRequest moduleLaunchRequest : moduleLaunchRequests) {
Coordinates moduleCoordinates
= toCoordinates(moduleLaunchRequest.getModule());
if (root == null) {
root = moduleCoordinates;
}
else {
includeCoordinates.add(toCoordinates(moduleLaunchRequest.getModule()));
}
Resource moduleResource = resolveModule(moduleLaunchRequest.getModule());
JarFileArchive moduleArchive = new JarFileArchive(moduleResource.getFile());
mainClassNames.add(moduleArchive.getMainClass());
arguments.add(toArgArray(moduleLaunchRequest.getArguments()));
}
for (String include :
StringUtils.commaDelimitedListToStringArray(aggregateArgs.get(INCLUDE_DEPENDENCIES_ARG))) {
includeCoordinates.add(toCoordinates(include));
}
// Resolve all artifacts - since modules have been specified as direct dependencies, they will take
// precedence in the resolution order, ensuring that the already resolved artifacts will be returned as
// part of the response.
Resource[] libraries = moduleResolver.resolve(root,
includeCoordinates.toArray(new Coordinates[includeCoordinates.size()]),
StringUtils.commaDelimitedListToStringArray(aggregateArgs.get(EXCLUDE_DEPENDENCIES_ARG)));
for (Resource library : libraries) {
jarURLs.add(library.getURL());
}
classLoader = new URLClassLoader(jarURLs.toArray(new URL[jarURLs.size()]));
}
final List<Class<?>> mainClasses = new ArrayList<>();
for (String mainClass : mainClassNames) {
mainClasses.add(ClassUtils.forName(mainClass, classLoader));
}
Runnable moduleAggregatorRunner = new ModuleAggregatorRunner(classLoader, mainClasses,
toArgArray(aggregateArgs), arguments);
Thread moduleAggregatorRunnerThread = new Thread(moduleAggregatorRunner);
moduleAggregatorRunnerThread.setContextClassLoader(classLoader);
moduleAggregatorRunnerThread.setName(MODULE_AGGREGATOR_RUNNER_THREAD_NAME);
moduleAggregatorRunnerThread.start();
} catch (Exception e) {
throw new RuntimeException("failed to start aggregated modules: " +
StringUtils.collectionToCommaDelimitedString(moduleLaunchRequests), e);
}
}
public void launchIndividualModules(List<ModuleLaunchRequest> moduleLaunchRequests) {
List<ModuleLaunchRequest> reversed = new ArrayList<>(moduleLaunchRequests);
Collections.reverse(reversed);
for (ModuleLaunchRequest moduleLaunchRequest : reversed) {
String module = moduleLaunchRequest.getModule();
Map<String, String> arguments = moduleLaunchRequest.getArguments();
if (arguments.containsKey(INCLUDE_DEPENDENCIES_ARG) || arguments.containsKey(EXCLUDE_DEPENDENCIES_ARG)) {
String includes = arguments.get(INCLUDE_DEPENDENCIES_ARG);
String excludes = arguments.get(EXCLUDE_DEPENDENCIES_ARG);
launchModuleWithDependencies(module, toArgArray(arguments),
StringUtils.commaDelimitedListToStringArray(includes),
StringUtils.commaDelimitedListToStringArray(excludes));
} else {
launchModule(module, toArgArray(arguments));
}
}
}
private void launchModule(String module, String[] args) {
try {
Resource resource = resolveModule(module);
JarFileArchive jarFileArchive = new JarFileArchive(resource.getFile());
ModuleJarLauncher jarLauncher = new ModuleJarLauncher(jarFileArchive);
jarLauncher.launch(args);
}
catch (IOException e) {
throw new RuntimeException("failed to launch module: " + module, e);
}
}
private void launchModuleWithDependencies(String module, String[] args, String[] includes, String[] excludes) {
try {
Resource[] libraries = this.moduleResolver.resolve(toCoordinates(module),
toCoordinateArray(includes), excludes);
List<Archive> archives = new ArrayList<>();
for (Resource library : libraries) {
archives.add(new JarFileArchive(library.getFile()));
}
MultiArchiveLauncher jarLauncher = new MultiArchiveLauncher(archives);
jarLauncher.launch(args);
}
catch (IOException e) {
throw new RuntimeException("failed to launch module: " + module, e);
}
}
private Resource resolveModule(String moduleCoordinates) {
Coordinates coordinates = toCoordinates(moduleCoordinates);
return this.moduleResolver.resolve(coordinates);
}
private Coordinates toCoordinates(String coordinates) {
Matcher matcher = COORDINATES_PATTERN.matcher(coordinates);
Assert.isTrue(matcher.matches(), "Bad artifact coordinates " + coordinates
+ ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
String groupId = matcher.group(1);
String artifactId = matcher.group(2);
String extension = StringUtils.hasLength(matcher.group(4)) ? matcher.group(4) : DEFAULT_EXTENSION;
String classifier = StringUtils.hasLength(matcher.group(6)) ? matcher.group(6) : DEFAULT_CLASSIFIER;
String version = matcher.group(7);
return new Coordinates(groupId, artifactId, extension, classifier, version);
}
private Coordinates[] toCoordinateArray(String[] coordinateList) {
List<Coordinates> result = new ArrayList<>();
for (String coordinates : coordinateList) {
result.add(toCoordinates(coordinates));
}
return result.toArray(new Coordinates[result.size()]);
}
private class ModuleAggregatorRunner implements Runnable {
private final ClassLoader classLoader;
private final String[] parentArgs;
private final List<Class<?>> mainClasses;
private final List<String[]> arguments;
public ModuleAggregatorRunner(ClassLoader classLoader, List<Class<?>> mainClasses, String[] parentArgs,
List<String[]> moduleArguments) {
this.classLoader = classLoader;
this.parentArgs = parentArgs;
this.mainClasses = mainClasses;
this.arguments = moduleArguments;
}
@Override
public void run() {
try {
// we expect the class and method to be found on the module classpath
Class<?> moduleAggregatorClass = ClassUtils.forName(AGGREGATE_APPLICATION_CLASS, classLoader);
Method aggregateMethod = ReflectionUtils.findMethod(moduleAggregatorClass,
AGGREGATE_APPLICATION_RUN_METHOD, Class[].class, String[].class, String[][].class);
aggregateMethod.invoke(null,
mainClasses.toArray(new Class<?>[mainClasses.size()]),
parentArgs, arguments.toArray(new String[][] {}));
} catch (Exception e) {
log.error("failed to launch aggregated modules :"
+ StringUtils.collectionToCommaDelimitedString(mainClasses), e);
throw new RuntimeException(e);
}
}
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2015-2016 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.launcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Bootstrap for launching one or more modules, provided via the "modules" system property
* or "MODULES" environment variable as a comma-delimited list, with the arguments
* provided at launch.
*
* @see ModuleLauncherProperties for module and argument structure and
* format
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
@SpringBootApplication
public class ModuleLauncherApplication {
public static void main(String[] args) throws Exception {
List<String> argsList = new ArrayList<>(Arrays.asList(args));
if (!argsList.contains("spring.jmx.default-domain")) {
argsList.add("--spring.jmx.default-domain=module-launcher");
}
SpringApplication.run(ModuleLauncherApplication.class, argsList.toArray(new String[0]));
}
}

View File

@@ -1,41 +0,0 @@
/*
* 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.launcher;
import org.springframework.cloud.stream.module.resolver.ModuleResolver;
import org.springframework.cloud.stream.module.resolver.ModuleResolverConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* Configuration class that has the beans required for module launcher.
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Eric Bottard
*/
@Configuration
@Import(ModuleResolverConfiguration.class)
public class ModuleLauncherConfiguration {
@Bean
public ModuleLauncher moduleLauncher(ModuleResolver moduleResolver) {
return new ModuleLauncher(moduleResolver);
}
}

View File

@@ -1,102 +0,0 @@
/*
* 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.launcher;
import java.util.HashMap;
import java.util.Map;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.env.Environment;
/**
* Configuration properties for {@link ModuleLauncher}.
*
* <p>Expects the following keys (resolved from the {@link Environment}, so this could take many forms _ System
* properties, environment variables, program arguments, <i>etc.</i> _):<ul>
* <li>{@literal modules = <list>}: an ordered list of maven coordinates of modules to launch</li>
* <li>{@literal args[<index>][<key>] = <value>}: key/value pairs that will become module arguments,
* where {@literal <index>} is the 0-based index of the module in the list above and '*' can be used for passing
* arguments to all modules</li>
* <li>{@literal aggregate = true | false}</li>: whether multiple modules launched together should be aggregated,
* case in which they will be launched as a single individual unit, and {@literal args['aggregate'][<key>] = <value>}
* can be used for passing arguments to the aggregate;
* </ul>
*
* As an example, this is how one would launch the {@literal time --fixedDelay=4 | log} canonical example:
* <pre>
* modules = org.springframework.cloud.modules:time-source:1.0.0-SNAPSHOT,org.springframework.cloud.modules:log-sink:1.0.0-SNAPSHOT
* args.0.fixedDelay=4
* </pre>
*
* And this is how one would launch the {@literal time --fixedDelay=4 | log} example as an aggregate:
* <pre>
* modules = org.springframework.cloud.modules:time-source:1.0.0-SNAPSHOT,org.springframework.cloud.modules:log-sink:1.0.0-SNAPSHOT
* args.0.fixedDelay=4
* aggregate=true
* </pre>
* </p>
*
* @author Ilayaperumal Gopinathan
* @author Marius Bogoevici
* @author Eric Bottard
*/
@ConfigurationProperties
public class ModuleLauncherProperties {
/**
* True if aggregating multiple modules when launched together
*/
private boolean aggregate;
/**
* File path to a locally available maven repository, where modules will be downloaded.
*/
private String[] modules;
/**
* Map of arguments, keyed by the 0-based index in the {@link #modules array}, allowing for an additional generic
* key for global arguments.
*/
private Map<String, Map<String, String>> args = new HashMap<>();
public boolean isAggregate() {
return aggregate;
}
public void setAggregate(boolean aggregate) {
this.aggregate = aggregate;
}
public void setModules(String[] modules) {
this.modules = modules;
}
@NotEmpty(message = "A list of modules must be specified.")
public String[] getModules() {
return modules;
}
public void setArgs(Map<String, Map<String, String>> args) {
this.args = args;
}
public Map<String, Map<String, String>> getArgs() {
return args;
}
}

View File

@@ -1,98 +0,0 @@
/*
* 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.launcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Spring boot {@link ApplicationRunner} that triggers {@link ModuleLauncher} to launch the modules.
*
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Eric Bottard
*/
@Component
@EnableConfigurationProperties(ModuleLauncherProperties.class)
public class ModuleLauncherRunner implements CommandLineRunner {
private final static Log log = LogFactory.getLog(ModuleLauncherRunner.class);
private final static String GLOBAL_ARGS_KEY = "*";
private final static String AGGREGATE_ARGS_KEY = "aggregate";
@Autowired
private ModuleLauncherProperties moduleLauncherProperties;
@Autowired
private ModuleLauncher moduleLauncher;
@Override
public void run(String... args) throws Exception {
List<ModuleLaunchRequest> launchRequests = generateModuleLaunchRequests();
if (log.isInfoEnabled()) {
StringBuilder sb = new StringBuilder("Launching\n");
for (ModuleLaunchRequest moduleLaunchRequest : launchRequests) {
sb.append('\t').append(moduleLaunchRequest).append('\n');
}
log.info(sb.toString());
}
this.moduleLauncher.launch(launchRequests,
moduleLauncherProperties.isAggregate(),
moduleLauncherProperties.isAggregate() ?
moduleLauncherProperties.getArgs().get(AGGREGATE_ARGS_KEY) :
Collections.<String, String>emptyMap());
}
private List<ModuleLaunchRequest> generateModuleLaunchRequests() {
List<ModuleLaunchRequest> requests = new ArrayList<>();
String[] modules = this.moduleLauncherProperties.getModules();
Map<String, Map<String, String>> arguments = this.moduleLauncherProperties.getArgs();
for (int i = 0; i < modules.length; i++) {
// merge the global arguments with the module specific arguments
Map<String, String> moduleArguments = new HashMap<>();
if (arguments != null) {
// by inserting the global args first and the module specific args next
// we ensure that the latter have precedence
Map<String, String> globalArgs = arguments.get(GLOBAL_ARGS_KEY);
if (globalArgs != null) {
moduleArguments.putAll(globalArgs);
}
Map<String, String> moduleSpecificArgs = arguments.get(Integer.toString(i));
if (moduleSpecificArgs != null) {
moduleArguments.putAll(moduleSpecificArgs);
}
}
ModuleLaunchRequest moduleLaunchRequest = new ModuleLaunchRequest(modules[i], moduleArguments);
requests.add(moduleLaunchRequest);
}
return requests;
}
}

View File

@@ -1,99 +0,0 @@
/*
* 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.launcher;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.loader.Launcher;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.cloud.stream.module.utils.ClassloaderUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* A {@link Launcher} for multiple independent JAR archives (which aren't nested in an uber jar). This class
* supports adding includes and excludes to a module's classpath.
*
* @author Marius Bogoevici
* @author Eric Bottard
*/
public class MultiArchiveLauncher extends Launcher {
private static Log log = LogFactory.getLog(MultiArchiveLauncher.class);
private final List<Archive> archives;
/**
* A list of archives, the first of which is expected to be a Spring boot uberJar
*
* @param archives
*/
public MultiArchiveLauncher(List<Archive> archives) {
Assert.notEmpty(archives, "A list of archives must be provided");
this.archives = archives;
}
@Override
protected String getMainClass() throws Exception {
return archives.get(0).getManifest().getMainAttributes().getValue("Start-Class");
}
@Override
protected List<Archive> getClassPathArchives() throws Exception {
return archives;
}
@Override
public void launch(String[] args) {
super.launch(args);
}
@Override
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
if (log.isDebugEnabled()) {
for (Archive archive : archives) {
log.debug("Launching with: " + archive.getUrl().toString());
}
}
if (ClassUtils.isPresent(
"org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
classLoader)) {
// Ensure the method is invoked on a class that is loaded by the provided
// class loader (not the current context class loader):
Method method = ReflectionUtils
.findMethod(
classLoader
.loadClass("org.apache.catalina.webresources.TomcatURLStreamHandlerFactory"),
"disable");
ReflectionUtils.invokeMethod(method, null);
}
super.launch(args, mainClass, classLoader);
}
@Override
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return ClassloaderUtils.createModuleClassloader(urls);
}
}

View File

@@ -1,302 +0,0 @@
/*
* Copyright 2015-2016 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.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.collection.CollectRequest;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.Proxy;
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.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
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.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.repository.DefaultProxySelector;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* An implementation of ModuleResolver using <a href="http://www.eclipse.org/aether/>aether</a> to resolve the module
* artifact (uber jar) in a local Maven repository, downloading the latest update from a remote repository if
* necessary.
*
* @author David Turanski
* @author Mark Fisher
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
*/
public class AetherModuleResolver implements ModuleResolver {
private static final Log log = LogFactory.getLog(AetherModuleResolver.class);
private static final String DEFAULT_CONTENT_TYPE = "default";
private final File localRepository;
private final List<RemoteRepository> remoteRepositories;
private final RepositorySystem repositorySystem;
private volatile boolean offline = false;
private final AetherProxyProperties proxyProperties;
private Authentication authentication;
/**
* 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.
* @param proxyProperties the proxy properties for the maven proxy settings.
*/
public AetherModuleResolver(File localRepository, Map<String, String> remoteRepositories,
final AetherProxyProperties proxyProperties) {
Assert.notNull(localRepository, "Local repository path cannot be null");
if (log.isDebugEnabled()) {
log.debug("Local repository: " + localRepository);
if (!CollectionUtils.isEmpty(remoteRepositories)) {
// just listing the values, ids are simply informative
log.debug("Remote repositories: " + StringUtils.collectionToCommaDelimitedString(remoteRepositories.values()));
}
}
this.proxyProperties = proxyProperties;
if (isProxyEnabled() && proxyHasCredentials()) {
this.authentication = new Authentication() {
@Override
public void fill(AuthenticationContext context, String key, Map<String, String> data) {
context.put(context.USERNAME, proxyProperties.getAuth().getUsername());
context.put(context.PASSWORD, proxyProperties.getAuth().getPassword());
}
@Override
public void digest(AuthenticationDigest digest) {
digest.update(AuthenticationContext.USERNAME, proxyProperties.getAuth().getUsername(),
AuthenticationContext.PASSWORD, proxyProperties.getAuth().getPassword());
}
};
}
if (!localRepository.exists()) {
Assert.isTrue(localRepository.mkdirs(),
"Unable to create directory for local repository: " + localRepository);
}
this.localRepository = localRepository;
this.remoteRepositories = new LinkedList<>();
if (!CollectionUtils.isEmpty(remoteRepositories)) {
for (Map.Entry<String, String> remoteRepo : remoteRepositories.entrySet()) {
RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder(remoteRepo.getKey(),
DEFAULT_CONTENT_TYPE, remoteRepo.getValue());
if (this.authentication != null) {
//todo: Set direct authentication for the remote repositories
remoteRepositoryBuilder.setProxy(new Proxy(proxyProperties.getProtocol(), proxyProperties.getHost(),
proxyProperties.getPort(), authentication));
}
this.remoteRepositories.add(remoteRepositoryBuilder.build());
}
}
repositorySystem = newRepositorySystem();
}
/**
* Check if the proxy settings are provided.
*
* @return boolean true if the proxy settings are provided.
*/
private boolean isProxyEnabled() {
return (this.proxyProperties != null && this.proxyProperties.getHost() != null && proxyProperties.getPort() > 0);
}
/**
* Check if the proxy setting has username/password set.
*
* @return boolean true if both the username/password are set
*/
private boolean proxyHasCredentials() {
return (this.proxyProperties != null && this.proxyProperties.getAuth() != null &&
this.proxyProperties.getAuth().getUsername() != null && this.proxyProperties.getAuth().getPassword() != null);
}
public void setOffline(boolean offline) {
this.offline = offline;
}
/**
* 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 coordinates the Maven coordinates of the artifact
* @return a {@link FileSystemResource} representing the resolved artifact in the local repository
* @throws RuntimeException if the artifact does not exist or the resolution fails
*/
@Override
public Resource resolve(Coordinates coordinates) {
return this.resolve(coordinates, null, null)[0];
}
/*
* 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));
session.setOffline(this.offline);
if (isProxyEnabled()) {
DefaultProxySelector proxySelector = new DefaultProxySelector();
Proxy proxy = new Proxy(proxyProperties.getProtocol(), proxyProperties.getHost(), proxyProperties.getPort(),
authentication);
proxySelector.add(proxy, proxyProperties.getNonProxyHosts());
session.setProxySelector(proxySelector);
}
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);
}
/**
* Resolve a set of artifacts based on their coordinates, including their dependencies, and return the locations of
* the transitive set in the local repository. Aether performs the normal Maven resolution process ensuring that the
* latest update is cached to the local repository. A number of additional includes and excludes can be specified,
* allowing to override the transitive dependencies of the original set. Includes and their transitive dependencies
* will always
*
* @param root the Maven coordinates of the artifacts
* @return a {@link FileSystemResource} representing the resolved artifact in the local repository
* @throws RuntimeException if the artifact does not exist or the resolution fails
*/
@Override
public Resource[] resolve(Coordinates root, Coordinates[] includes, String[] excludePatterns) {
Assert.notNull(root, "Root cannot be null");
validateCoordinates(root);
if (!ObjectUtils.isEmpty(includes)) {
for (Coordinates include : includes) {
Assert.notNull(include, "Includes cannot be null");
validateCoordinates(include);
}
}
List<Resource> result = new ArrayList<>();
Artifact rootArtifact = toArtifact(root);
RepositorySystemSession session = newRepositorySystemSession(repositorySystem,
localRepository.getAbsolutePath());
if (ObjectUtils.isEmpty(includes) && ObjectUtils.isEmpty(excludePatterns)) {
ArtifactResult resolvedArtifact;
try {
resolvedArtifact = repositorySystem.resolveArtifact(session,
new ArtifactRequest(rootArtifact, remoteRepositories, JavaScopes.RUNTIME));
}
catch (ArtifactResolutionException e) {
throw new RuntimeException(e);
}
result.add(toResource(resolvedArtifact));
}
else {
try {
CollectRequest collectRequest = new CollectRequest();
collectRequest.setRepositories(remoteRepositories);
collectRequest.setRoot(new Dependency(rootArtifact, JavaScopes.RUNTIME));
Artifact[] includeArtifacts = new Artifact[!ObjectUtils.isEmpty(includes) ? includes.length : 0];
int i = 0;
for (Coordinates include : includes) {
Artifact includedArtifact = toArtifact(include);
collectRequest.addDependency(new Dependency(includedArtifact, JavaScopes.RUNTIME));
includeArtifacts[i++] = includedArtifact;
}
DependencyResult dependencyResult =
repositorySystem.resolveDependencies(session,
new DependencyRequest(collectRequest,
new ModuleDependencyFilter(includeArtifacts, excludePatterns)));
for (ArtifactResult artifactResult : dependencyResult.getArtifactResults()) {
// we are only interested in the jars
if ("jar".equalsIgnoreCase(artifactResult.getArtifact().getExtension())) {
result.add(toResource(artifactResult));
}
}
}
catch (DependencyResolutionException e) {
throw new RuntimeException(e);
}
}
return result.toArray(new Resource[result.size()]);
}
private void validateCoordinates(Coordinates coordinates) {
Assert.hasText(coordinates.getGroupId(), "'groupId' cannot be blank.");
Assert.hasText(coordinates.getArtifactId(), "'artifactId' cannot be blank.");
Assert.hasText(coordinates.getExtension(), "'extension' cannot be blank.");
Assert.hasText(coordinates.getVersion(), "'version' cannot be blank.");
}
public FileSystemResource toResource(ArtifactResult resolvedArtifact) {
return new FileSystemResource(resolvedArtifact.getArtifact().getFile());
}
private Artifact toArtifact(Coordinates root) {
return new DefaultArtifact(root.getGroupId(),
root.getArtifactId(),
root.getClassifier() != null ? root.getClassifier() : "",
root.getExtension(),
root.getVersion());
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2016 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 org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Proxy properties for the Aether Module Resolver.
*
* @author Ilayaperumal Gopinathan
*/
@ConfigurationProperties(prefix = "aether.proxy")
public class AetherProxyProperties {
/**
* Protocol to use for proxy settings.
*/
private String protocol = "http";
/**
* Host for the proxy.
*/
private String host;
/**
* Port for the proxy.
*/
private int port;
/**
* List of non proxy hosts.
*/
private String nonProxyHosts;
private Authentication auth;
public String getProtocol() {
return this.protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
public String getNonProxyHosts() {
return this.nonProxyHosts;
}
public void setNonProxyHosts(String nonProxyHosts) {
this.nonProxyHosts = nonProxyHosts;
}
public Authentication getAuth() {
return this.auth;
}
public void setAuth(Authentication auth) {
this.auth = auth;
}
public static class Authentication {
/**
* Username for the proxy.
*/
private String username;
/**
* Password for the proxy.
*/
private String password;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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;
/**
* Encapsulates Maven coordinates.
*
* @author Marius Bogoevici
* @author Eric Bottard
*/
public class Coordinates {
private final String groupId;
private final String artifactId;
private final String extension;
private String classifier;
private final String version;
/**
* @param groupId the groupId
* @param artifactId the artifactId
* @param extension the file extension
* @param classifier classifier
* @param version the version
*/
public Coordinates(String groupId, String artifactId, String extension, String classifier, String version) {
this.groupId = groupId;
this.artifactId = artifactId;
this.extension = extension;
this.classifier = classifier;
this.version = version;
}
public String getGroupId() {
return groupId;
}
public String getArtifactId() {
return artifactId;
}
public String getExtension() {
return extension;
}
public String getClassifier() {
return classifier;
}
public String getVersion() {
return version;
}
}

View File

@@ -1,78 +0,0 @@
/*
* 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.util.List;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter;
import org.springframework.util.ObjectUtils;
/**
* A {@link DependencyFilter} that uses a list of explicit includes and pattern-based excludes. Any items that match
* the exclusion pattern will be rejected, unless they are accepted explicitly
*
* @author Marius Bogoevici
*/
public class ModuleDependencyFilter implements DependencyFilter {
private final PatternExclusionsDependencyFilter patternExclusionsDependencyFilter;
private final Artifact[] includes;
private boolean acceptOptional = false;
public ModuleDependencyFilter(Artifact[] includes, String... excludes) {
this.patternExclusionsDependencyFilter = new PatternExclusionsDependencyFilter(excludes);
this.includes = includes != null ? includes : new Artifact[0];
}
public void setAcceptOptional(boolean acceptOptional) {
this.acceptOptional = acceptOptional;
}
@Override
public boolean accept(DependencyNode node, List<DependencyNode> parents) {
// optional nodes are rejected conditionally
// nodes included explicitly are always accepted
return (acceptOptional || !node.getDependency().isOptional())
&& (isIncludedDirectly(node) || patternExclusionsDependencyFilter.accept(node, parents));
}
private boolean isIncludedDirectly(DependencyNode node) {
if (node.getArtifact() != null) {
for (Artifact include : includes) {
Artifact nodeArtifact = node.getArtifact();
// we check if this was a specifically included artifact by checking its group, artifactId, extension and
// classifier. The version is left out in the case when resolution produces a different artifact version
// (there cannot be two artifacts with the same group, artifactId, extension and classifier but different
// version in the resolved group)
if (ObjectUtils.nullSafeEquals(include.getGroupId(), nodeArtifact.getGroupId())
&& ObjectUtils.nullSafeEquals(include.getArtifactId(), nodeArtifact.getArtifactId())
&& ObjectUtils.nullSafeEquals(include.getClassifier(), nodeArtifact.getClassifier())
&& ObjectUtils.nullSafeEquals(include.getExtension(), nodeArtifact.getExtension())) {
return true;
}
}
return false;
}
return true;
}
}

View File

@@ -1,52 +0,0 @@
/*
* 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 org.springframework.core.io.Resource;
/**
* Interface used to return a {@link Resource} that provides access to a module
* uber-jar based on its Maven coordinates.
*
* @author David Turanski
* @author Marius Bogoevici
* @author Eric Bottard
*/
public interface ModuleResolver {
/**
* Retrieve a resource given its coordinates.
*
* @param coordinates the coordinates of a resource
* @return the resource
*/
Resource resolve(Coordinates coordinates);
/**
* Retrieve a set of resources given their coordinates, along with additional dependencies.
* Exclusion rules patterns (conforming to {@link org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter}
* can be provided as well.
*
* @param root the coordinates of the main resource
* @param includes a list of coordinates to include along the main resource
* @param excludePatterns a list of exclusion patterns
* @see org.eclipse.aether.util.filter.PatternExclusionsDependencyFilter
* @return the main resource and the additional dependencies
*/
Resource[] resolve(Coordinates root, Coordinates[] includes, String[] excludePatterns);
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2015-2016 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.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Sets up the default Aether-based module resolver, unless overridden.
*
* @author Eric Bottard
* @author Ilayaperumal Gopinathan
*/
@EnableConfigurationProperties({ModuleResolverProperties.class, AetherProxyProperties.class})
public class ModuleResolverConfiguration {
@Autowired
private ModuleResolverProperties properties;
@Autowired
private AetherProxyProperties proxyProperties;
@Bean
@ConditionalOnMissingBean(ModuleResolver.class)
public ModuleResolver moduleResolver() {
int i = 1;
Map<String, String> repositoriesMap = new HashMap<>();
for (String repository : properties.getRemoteRepositories()) {
repositoriesMap.put("repository " + i++, repository);
}
AetherModuleResolver aetherModuleResolver = new AetherModuleResolver(properties.getLocalRepository(),
repositoriesMap, proxyProperties);
aetherModuleResolver.setOffline(properties.isOffline());
return aetherModuleResolver;
}
}

View File

@@ -1,70 +0,0 @@
/*
* 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 org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for {@link ModuleResolver}.
*
* @author Ilayaperumal Gopinathan
*/
@ConfigurationProperties
public class ModuleResolverProperties {
/**
* File path to a locally available maven repository, where modules will be downloaded.
*/
private File localRepository = new File(System.getProperty("user.home") + File.separator + ".m2" +
File.separator + "repository");
/**
* Location of comma separated remote maven repositories from which modules will be downloaded, if not available locally.
*/
private String[] remoteRepositories = new String[]{"https://repo.spring.io/libs-snapshot"};
/**
* Whether the resolver should operate in offline mode.
*/
private boolean offline = false;
public void setRemoteRepositories(String[] remoteRepositories) {
this.remoteRepositories = remoteRepositories;
}
public String[] getRemoteRepositories() {
return remoteRepositories;
}
public void setLocalRepository(File localRepository) {
this.localRepository = localRepository;
}
public File getLocalRepository() {
return localRepository;
}
public boolean isOffline() {
return offline;
}
public void setOffline(boolean offline) {
this.offline = offline;
}
}

View File

@@ -1,5 +0,0 @@
/**
* Root package of the ModuleResolver support.
*/
package org.springframework.cloud.stream.module.resolver;

View File

@@ -1,71 +0,0 @@
/*
* 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.utils;
import java.net.URL;
import java.net.URLClassLoader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import org.springframework.util.StringUtils;
/**
* @author Marius Bogoevici
*/
public class ClassloaderUtils {
private static final Log log = LogFactory.getLog(ClassloaderUtils.class);
/**
* Creates a ClassLoader for the launched modules by merging the URLs supplied as argument with the URLs that
* make up the additional classpath of the launched JVM (retrieved from the application classloader), and
* setting the extension classloader of the JVM as parent, if accessible.
*
* @param urls a list of library URLs
* @return the resulting classloader
*/
public static ClassLoader createModuleClassloader(URL[] urls) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
if (log.isDebugEnabled()) {
log.debug("systemClassLoader is " + systemClassLoader);
}
if (systemClassLoader instanceof URLClassLoader) {
// add the URLs of the application classloader to the created classloader
// to compensate for LaunchedURLClassLoader not delegating to parent to retrieve resources
@SuppressWarnings("resource")
URLClassLoader systemUrlClassLoader = (URLClassLoader) systemClassLoader;
URL[] mergedUrls = new URL[urls.length + systemUrlClassLoader.getURLs().length];
if (log.isDebugEnabled()) {
log.debug("Original URLs: " +
StringUtils.arrayToCommaDelimitedString(urls));
log.debug("Java Classpath URLs: " +
StringUtils.arrayToCommaDelimitedString(systemUrlClassLoader.getURLs()));
}
System.arraycopy(urls, 0, mergedUrls, 0, urls.length);
System.arraycopy(systemUrlClassLoader.getURLs(), 0, mergedUrls, urls.length,
systemUrlClassLoader.getURLs().length);
// add the extension classloader as parent to the created context, if accessible
if (log.isDebugEnabled()) {
log.debug("Classloader URLs: " + StringUtils.arrayToCommaDelimitedString(mergedUrls));
}
return new LaunchedURLClassLoader(mergedUrls, systemUrlClassLoader.getParent());
}
return new LaunchedURLClassLoader(urls, systemClassLoader);
}
}

View File

@@ -1,150 +0,0 @@
/*
* 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 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.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.object.HasToString.hasToString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.eclipse.aether.resolution.ArtifactResolutionException;
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 com.github.tomakehurst.wiremock.junit.WireMockRule;
/**
* @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, null);
Resource resource = defaultModuleResolver.resolve(new Coordinates("foo.bar", "foo-bar", "jar", "", "1.0.0"));
assertTrue(resource.exists());
assertEquals(resource.getFile().getName(), "foo-bar-1.0.0.jar");
}
@Test
public void testResolveLocalWithIncludes() throws IOException {
ClassPathResource cpr = new ClassPathResource("local-repo");
File localRepository = cpr.getFile();
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, null, null);
Resource[] resources = defaultModuleResolver.resolve(
new Coordinates("foo.bar", "foo-bar", "jar", "", "1.0.0"),
new Coordinates[]{new Coordinates("qux.bar", "qux-bar", "jar", "", "1.0.0")},new String[]{});
assertThat(resources, arrayWithSize(2));
assertTrue(resources[0].exists());
assertTrue(resources[1].exists());
// the optional dependency 'foo.baz:foo-baz:1.0.0'of 'foo.bar:foo-bar' is not included
assertThat(resources, arrayContainingInAnyOrder(
hasToString(containsString("foo-bar-1.0.0.jar")),
hasToString(containsString("qux-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, null);
defaultModuleResolver.resolve(new Coordinates("niente", "nada", "jar", "", "zilch"));
}
@Test
@Ignore
public void testResolveRemote() throws IOException {
File localRepository = Files.createTempDirectory(UUID.randomUUID().toString()).toFile();
localRepository.deleteOnExit();
Map<String, String> remoteRepos = new HashMap<>();
remoteRepos.put("modules", "http://repo.spring.io/libs-snapshot");
AetherModuleResolver defaultModuleResolver = new AetherModuleResolver(localRepository, remoteRepos, null);
Resource resource = defaultModuleResolver.resolve(
new Coordinates("org.springframework.cloud.stream.module", "time-source", "jar", "exec", "1.0.0.BUILD-SNAPSHOT"));
assertTrue(resource.exists());
assertEquals(resource.getFile().getName(), "time-source-1.0.0.BUILD-SNAPSHOT-exec.jar");
}
@Test
public void testResolveMockRemote() throws IOException {
File localRepository = Files.createTempDirectory(UUID.randomUUID().toString()).toFile();
localRepository.deleteOnExit();
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, null);
Resource resource = defaultModuleResolver.resolve(new Coordinates("org.bar", "foo", "jar", "", "1.0.0"));
assertTrue(resource.exists());
assertEquals(resource.getFile().getName(), "foo-1.0.0.jar");
}
@Test
public void testResolveOfflineWithMockRemote() throws IOException {
try {
File localRepository = Files.createTempDirectory(UUID.randomUUID().toString()).toFile();
localRepository.deleteOnExit();
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, null);
defaultModuleResolver.setOffline(true);
defaultModuleResolver.resolve(new Coordinates("org.bar", "foo", "jar", "", "1.0.0"));
} catch (RuntimeException e) {
// remote resolution fails because the resolver is operating offline
assertThat(e.getCause(), instanceOf(ArtifactResolutionException.class));
}
}
}

View File

@@ -1,20 +0,0 @@
<?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>
<groupId>foo.bar</groupId>
<artifactId>foo-bar</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>foo-bar</name>
<dependencies>
<dependency>
<groupId>foo.baz</groupId>
<artifactId>foo-baz</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -1,12 +0,0 @@
<?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>
<groupId>foo.baz</groupId>
<artifactId>foo-baz</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>foo-baz</name>
</project>

View File

@@ -1,12 +0,0 @@
<?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>
<groupId>qux-bar</groupId>
<artifactId>qux-bar</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>qux-bar</name>
</project>