diff --git a/spring-cloud-function-compiler/pom.xml b/spring-cloud-function-compiler/pom.xml
index 09ac1974d..799611b9a 100644
--- a/spring-cloud-function-compiler/pom.xml
+++ b/spring-cloud-function-compiler/pom.xml
@@ -46,6 +46,31 @@
spring-boot-starter-web
true
+
+ org.apache.maven
+ maven-core
+ 3.3.9
+
+
+ org.apache.maven
+ maven-embedder
+ 3.3.9
+
+
+ org.eclipse.aether
+ aether-transport-file
+ 1.0.2.v20150114
+
+
+ org.eclipse.aether
+ aether-transport-http
+ 1.0.2.v20150114
+
+
+ org.eclipse.aether
+ aether-connector-basic
+ 1.0.2.v20150114
+
org.springframework.boot
spring-boot-starter-test
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java
index 96215fcff..829cda196 100644
--- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java
@@ -16,6 +16,7 @@
package org.springframework.cloud.function.compiler.java;
+import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -41,6 +42,8 @@ public class CompilationResult {
List> compiledClasses = new ArrayList<>();
private Map classBytes = new HashMap<>();
+
+ private List resolvedAdditionalDependencies = new ArrayList<>();
public CompilationResult(boolean successfulCompilation) {
this.successfulCompilation = successfulCompilation;
@@ -49,6 +52,14 @@ public class CompilationResult {
public void addClassBytes(String name, byte[] bytes) {
this.classBytes.put(name, bytes);
}
+
+ public void setResolvedAdditionalDependencies(List resolvedAdditionalDependencies) {
+ this.resolvedAdditionalDependencies = resolvedAdditionalDependencies;
+ }
+
+ public List getResolvedAdditionalDependencies() {
+ return this.resolvedAdditionalDependencies;
+ }
public byte[] getClassBytes(String classname) {
return this.classBytes.get(classname);
@@ -66,8 +77,12 @@ public class CompilationResult {
return Collections.unmodifiableList(compilationMessages);
}
- public void recordCompilationMessage(CompilationMessage compilationMessage) {
- this.compilationMessages.add(compilationMessage);
+ public void recordCompilationMessage(CompilationMessage message) {
+ this.compilationMessages.add(message);
+ }
+
+ public void recordCompilationMessages(List messages) {
+ this.compilationMessages.addAll(messages);
}
public void setCompiledClasses(List> compiledClasses) {
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java
new file mode 100644
index 000000000..7e0b5d147
--- /dev/null
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016-2017 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.function.compiler.java;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.aether.repository.Proxy;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Composite {@link ProxySelector}.
+ *
+ * @author Dave Syer
+ */
+public class CompositeProxySelector implements ProxySelector {
+
+ private List selectors = new ArrayList();
+
+ public CompositeProxySelector(List selectors) {
+ this.selectors = selectors;
+ }
+
+ @Override
+ public Proxy getProxy(RemoteRepository repository) {
+ for (ProxySelector selector : this.selectors) {
+ Proxy proxy = selector.getProxy(repository);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java
new file mode 100644
index 000000000..c5a5f87ba
--- /dev/null
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2016-2017 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.function.compiler.java;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.inject.Singleton;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
+import org.apache.maven.artifact.repository.MavenArtifactRepository;
+import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.DefaultModelReader;
+import org.apache.maven.model.io.ModelReader;
+import org.apache.maven.model.locator.DefaultModelLocator;
+import org.apache.maven.model.locator.ModelLocator;
+import org.apache.maven.model.validation.DefaultModelValidator;
+import org.apache.maven.model.validation.ModelValidator;
+import org.apache.maven.project.DefaultProjectBuildingRequest;
+import org.apache.maven.project.DependencyResolutionResult;
+import org.apache.maven.project.ProjectBuilder;
+import org.apache.maven.project.ProjectBuildingException;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.project.ProjectBuildingRequest.RepositoryMerging;
+import org.apache.maven.project.ProjectBuildingResult;
+import org.apache.maven.repository.internal.DefaultArtifactDescriptorReader;
+import org.apache.maven.repository.internal.DefaultVersionRangeResolver;
+import org.apache.maven.repository.internal.DefaultVersionResolver;
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.apache.maven.repository.internal.SnapshotMetadataGeneratorFactory;
+import org.apache.maven.repository.internal.VersionsMetadataGeneratorFactory;
+import org.apache.maven.settings.Profile;
+import org.apache.maven.settings.Repository;
+import org.codehaus.plexus.ContainerConfiguration;
+import org.codehaus.plexus.DefaultContainerConfiguration;
+import org.codehaus.plexus.DefaultPlexusContainer;
+import org.codehaus.plexus.MutablePlexusContainer;
+import org.codehaus.plexus.PlexusConstants;
+import org.codehaus.plexus.PlexusContainer;
+import org.codehaus.plexus.classworlds.ClassWorld;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.MetadataGeneratorFactory;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.impl.VersionResolver;
+import org.eclipse.aether.impl.guice.AetherModule;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryPolicy;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.transport.file.FileTransporterFactory;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+import org.eclipse.aether.util.repository.JreProxySelector;
+import org.eclipse.sisu.inject.DefaultBeanLocator;
+import org.eclipse.sisu.plexus.ClassRealmManager;
+
+import org.springframework.core.io.Resource;
+import org.springframework.util.StringUtils;
+
+public class DependencyResolver {
+
+ private static DependencyResolver instance = new DependencyResolver();
+
+ private static Properties globals;
+
+ private LocalRepositoryManagerFactory localRepositoryManagerFactory;
+
+ private PlexusContainer container;
+
+ private final Object lock = new Object();
+
+ private ProjectBuilder projectBuilder;
+
+ private RepositorySystem repositorySystem;
+
+ private MavenSettings settings;
+
+ public static DependencyResolver instance() {
+ return instance;
+ }
+
+ public static void close() {
+ instance = new DependencyResolver();
+ }
+
+ private DependencyResolver() {
+ }
+
+ private void initialize() {
+ if (this.container == null) {
+ synchronized (lock) {
+ if (this.container == null) {
+ ClassWorld classWorld = new ClassWorld("plexus.core",
+ Thread.currentThread().getContextClassLoader());
+ ContainerConfiguration config = new DefaultContainerConfiguration()
+ .setClassWorld(classWorld)
+ .setRealm(classWorld.getClassRealm("plexus.core"))
+ .setClassPathScanning(PlexusConstants.SCANNING_INDEX)
+ .setAutoWiring(true).setName("maven");
+ PlexusContainer container;
+ try {
+ container = new DefaultPlexusContainer(config, new AetherModule(),
+ new DependencyResolutionModule());
+ localRepositoryManagerFactory = container
+ .lookup(LocalRepositoryManagerFactory.class);
+ container.addComponent(
+ new ClassRealmManager((MutablePlexusContainer) container,
+ new DefaultBeanLocator()),
+ ClassRealmManager.class.getName());
+ projectBuilder = container.lookup(ProjectBuilder.class);
+ repositorySystem = container.lookup(RepositorySystem.class);
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Cannot create container", e);
+ }
+ this.container = container;
+ this.settings = new MavenSettingsReader().readSettings();
+ }
+ }
+ }
+ }
+
+ public List dependencies(Resource resource) {
+ return dependencies(resource, new Properties());
+ }
+
+ public List dependencies(final Resource resource,
+ final Properties properties) {
+ initialize();
+ try {
+ ProjectBuildingRequest request = getProjectBuildingRequest(properties);
+ request.setResolveDependencies(true);
+ synchronized (DependencyResolver.class) {
+ ProjectBuildingResult result = projectBuilder
+ .build(new PropertiesModelSource(properties, resource), request);
+ DependencyResolver.globals = null;
+ DependencyResolutionResult dependencies = result
+ .getDependencyResolutionResult();
+ if (!dependencies.getUnresolvedDependencies().isEmpty()) {
+ StringBuilder builder = new StringBuilder();
+ for (Dependency dependency : dependencies
+ .getUnresolvedDependencies()) {
+ List errors = dependencies
+ .getResolutionErrors(dependency);
+ for (Exception exception : errors) {
+ if (builder.length() > 0) {
+ builder.append("\n");
+ }
+ builder.append(exception.getMessage());
+ }
+ }
+ throw new RuntimeException(builder.toString());
+ }
+ return runtime(dependencies.getDependencies());
+ }
+ }
+ catch (ProjectBuildingException | NoLocalRepositoryManagerException e) {
+ throw new IllegalStateException("Cannot build model", e);
+ }
+ }
+
+ public File resolve(Dependency dependency) {
+ initialize();
+ return collectNonTransitive(Arrays.asList(dependency)).iterator().next()
+ .getArtifact().getFile();
+ }
+
+ private List runtime(List dependencies) {
+ List list = new ArrayList<>();
+ for (Dependency dependency : dependencies) {
+ if (!"test".equals(dependency.getScope())
+ && !"provided".equals(dependency.getScope())) {
+ list.add(dependency);
+ }
+ }
+ return list;
+ }
+
+ private ProjectBuildingRequest getProjectBuildingRequest(Properties properties)
+ throws NoLocalRepositoryManagerException {
+ DefaultProjectBuildingRequest projectBuildingRequest = new DefaultProjectBuildingRequest();
+ DefaultRepositorySystemSession session = createSession(properties);
+ projectBuildingRequest.setRepositoryMerging(RepositoryMerging.REQUEST_DOMINANT);
+ projectBuildingRequest.setRemoteRepositories(mavenRepositories(properties));
+ projectBuildingRequest.getRemoteRepositories()
+ .addAll(mavenRepositories(settings));
+ projectBuildingRequest.setRepositorySession(session);
+ projectBuildingRequest.setProcessPlugins(false);
+ projectBuildingRequest.setBuildStartTime(new Date());
+ projectBuildingRequest.setUserProperties(properties);
+ projectBuildingRequest.setSystemProperties(System.getProperties());
+ return projectBuildingRequest;
+ }
+
+ private Collection extends ArtifactRepository> mavenRepositories(
+ MavenSettings settings) {
+ List list = new ArrayList<>();
+ for (Profile profile : settings.getActiveProfiles()) {
+ for (Repository repository : profile.getRepositories()) {
+ addRepositoryIfMissing(list, repository.getId(), repository.getUrl(),
+ repository.getReleases() != null
+ ? repository.getReleases().isEnabled() : true,
+ repository.getSnapshots() != null
+ ? repository.getSnapshots().isEnabled() : false);
+ }
+ }
+ return list;
+ }
+
+ private List mavenRepositories(Properties properties) {
+ List list = new ArrayList<>();
+ addRepositoryIfMissing(list, "spring-snapshots", "https://repo.spring.io/libs-snapshot", true, true);
+ addRepositoryIfMissing(list, "central", "https://repo1.maven.org/maven2", true, false);
+ return list;
+ }
+
+ private List aetherRepositories(Properties properties) {
+ List list = new ArrayList<>();
+ for (ArtifactRepository input : mavenRepositories(properties)) {
+ list.add(remote(input));
+ }
+ return list;
+ }
+
+ private RemoteRepository remote(ArtifactRepository input) {
+ return new RemoteRepository.Builder(input.getId(), input.getLayout().getId(),
+ input.getUrl()).setSnapshotPolicy(policy(input.getSnapshots()))
+ .setReleasePolicy(policy(input.getReleases())).build();
+ }
+
+ private RepositoryPolicy policy(ArtifactRepositoryPolicy input) {
+ RepositoryPolicy policy = new RepositoryPolicy(input.isEnabled(),
+ RepositoryPolicy.UPDATE_POLICY_DAILY,
+ RepositoryPolicy.CHECKSUM_POLICY_WARN);
+ return policy;
+ }
+
+ private void addRepositoryIfMissing(List list, String id,
+ String url, boolean releases, boolean snapshots) {
+ for (ArtifactRepository repo : list) {
+ if (url.equals(repo.getUrl())) {
+ return;
+ }
+ if (id.equals(repo.getId())) {
+ return;
+ }
+ }
+ list.add(repo(id, url, releases, snapshots));
+ }
+
+ private ArtifactRepository repo(String id, String url, boolean releases,
+ boolean snapshots) {
+ MavenArtifactRepository repository = new MavenArtifactRepository();
+ repository.setLayout(new DefaultRepositoryLayout());
+ repository.setId(id);
+ repository.setUrl(url);
+ ArtifactRepositoryPolicy enabled = new ArtifactRepositoryPolicy();
+ enabled.setEnabled(true);
+ ArtifactRepositoryPolicy disabled = new ArtifactRepositoryPolicy();
+ disabled.setEnabled(false);
+ repository.setReleaseUpdatePolicy(releases ? enabled : disabled);
+ repository.setSnapshotUpdatePolicy(snapshots ? enabled : disabled);
+ return repository;
+ }
+
+ private DefaultRepositorySystemSession createSession(Properties properties)
+ throws NoLocalRepositoryManagerException {
+ DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
+ LocalRepository repository = localRepository(properties);
+ session.setLocalRepositoryManager(
+ localRepositoryManagerFactory.newInstance(session, repository));
+ applySettings(session);
+ ProxySelector existing = session.getProxySelector();
+ if (existing == null || !(existing instanceof CompositeProxySelector)) {
+ JreProxySelector fallback = new JreProxySelector();
+ ProxySelector selector = existing == null ? fallback
+ : new CompositeProxySelector(Arrays.asList(existing, fallback));
+ session.setProxySelector(selector);
+ }
+ return session;
+ }
+
+ private void applySettings(DefaultRepositorySystemSession session) {
+ MavenSettingsReader.applySettings(settings, session);
+ }
+
+ private LocalRepository localRepository(Properties properties) {
+ return new LocalRepository(getM2RepoDirectory());
+ }
+
+ public Model readModel(Resource resource) {
+ return readModel(resource, new Properties());
+ }
+
+ public Model readModel(final Resource resource, final Properties properties) {
+ initialize();
+ try {
+ ProjectBuildingRequest request = getProjectBuildingRequest(properties);
+ request.setResolveDependencies(false);
+ ProjectBuildingResult result = projectBuilder
+ .build(new PropertiesModelSource(properties, resource), request);
+ return result.getProject().getModel();
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Failed to build model from effective pom",
+ e);
+ }
+ }
+
+ private File getM2RepoDirectory() {
+ return new File(getDefaultM2HomeDirectory(), "repository");
+ }
+
+ private File getDefaultM2HomeDirectory() {
+ String mavenRoot = System.getProperty("maven.home");
+ if (StringUtils.hasLength(mavenRoot)) {
+ return new File(mavenRoot);
+ }
+ return new File(System.getProperty("user.home"), ".m2");
+ }
+
+ private List collectNonTransitive(List dependencies) {
+ try {
+ List artifactRequests = getArtifactRequests(dependencies);
+ List result = this.repositorySystem
+ .resolveArtifacts(createSession(new Properties()), artifactRequests);
+ return result;
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private List getArtifactRequests(List dependencies) {
+ List list = new ArrayList<>();
+ for (Dependency dependency : dependencies) {
+ ArtifactRequest request = new ArtifactRequest(dependency.getArtifact(), null,
+ null);
+ request.setRepositories(aetherRepositories(new Properties()));
+ list.add(request);
+ }
+ return list;
+ }
+
+ static Properties getGlobals() {
+ return globals;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final class PropertiesModelSource
+ implements org.apache.maven.model.building.ModelSource {
+ private final Properties properties;
+
+ private final Resource resource;
+
+ private PropertiesModelSource(Properties properties, Resource resource) {
+ this.properties = properties;
+ this.resource = resource;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ DependencyResolver.globals = properties;
+ return new BufferedInputStream(resource.getInputStream()) {
+ @Override
+ public void close() throws IOException {
+ DependencyResolver.globals = null;
+ super.close();
+ }
+ };
+ }
+
+ @Override
+ public String getLocation() {
+ return resource.getDescription();
+ }
+ }
+
+}
+
+class DependencyResolutionModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ bind(ModelLocator.class).to(DefaultModelLocator.class).in(Singleton.class);
+ bind(ModelReader.class).to(DefaultModelReader.class).in(Singleton.class);
+ bind(ModelValidator.class).to(DefaultModelValidator.class).in(Singleton.class);
+ bind(RepositoryConnectorFactory.class).to(BasicRepositoryConnectorFactory.class)
+ .in(Singleton.class);
+ bind(ArtifactDescriptorReader.class) //
+ .to(DefaultArtifactDescriptorReader.class).in(Singleton.class);
+ bind(VersionResolver.class) //
+ .to(DefaultVersionResolver.class).in(Singleton.class);
+ bind(VersionRangeResolver.class) //
+ .to(DefaultVersionRangeResolver.class).in(Singleton.class);
+ bind(MetadataGeneratorFactory.class).annotatedWith(Names.named("snapshot")) //
+ .to(SnapshotMetadataGeneratorFactory.class).in(Singleton.class);
+ bind(MetadataGeneratorFactory.class).annotatedWith(Names.named("versions")) //
+ .to(VersionsMetadataGeneratorFactory.class).in(Singleton.class);
+ bind(TransporterFactory.class).annotatedWith(Names.named("http"))
+ .to(HttpTransporterFactory.class).in(Singleton.class);
+ bind(TransporterFactory.class).annotatedWith(Names.named("file"))
+ .to(FileTransporterFactory.class).in(Singleton.class);
+ }
+
+ @Provides
+ @Singleton
+ Set provideMetadataGeneratorFactories(
+ @Named("snapshot") MetadataGeneratorFactory snapshot,
+ @Named("versions") MetadataGeneratorFactory versions) {
+ Set factories = new HashSet<>();
+ factories.add(snapshot);
+ factories.add(versions);
+ return Collections.unmodifiableSet(factories);
+ }
+
+ @Provides
+ @Singleton
+ Set provideRepositoryConnectorFactories(
+ RepositoryConnectorFactory factory) {
+ return Collections.singleton(factory);
+ }
+
+ @Provides
+ @Singleton
+ Set provideTransporterFactories(
+ @Named("file") TransporterFactory file,
+ @Named("http") TransporterFactory http) {
+ // Order is decided elsewhere (by priority)
+ Set factories = new HashSet();
+ factories.add(file);
+ factories.add(http);
+ return Collections.unmodifiableSet(factories);
+ }
+
+}
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java
new file mode 100644
index 000000000..61a56d677
--- /dev/null
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2012-2017 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.function.compiler.java;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.model.ActivationFile;
+import org.apache.maven.model.ActivationOS;
+import org.apache.maven.model.ActivationProperty;
+import org.apache.maven.model.building.ModelProblemCollector;
+import org.apache.maven.model.building.ModelProblemCollectorRequest;
+import org.apache.maven.model.path.DefaultPathTranslator;
+import org.apache.maven.model.profile.DefaultProfileSelector;
+import org.apache.maven.model.profile.ProfileActivationContext;
+import org.apache.maven.model.profile.activation.FileProfileActivator;
+import org.apache.maven.model.profile.activation.JdkVersionProfileActivator;
+import org.apache.maven.model.profile.activation.OperatingSystemProfileActivator;
+import org.apache.maven.model.profile.activation.PropertyProfileActivator;
+import org.apache.maven.settings.Activation;
+import org.apache.maven.settings.Mirror;
+import org.apache.maven.settings.Profile;
+import org.apache.maven.settings.Proxy;
+import org.apache.maven.settings.Server;
+import org.apache.maven.settings.Settings;
+import org.apache.maven.settings.crypto.SettingsDecryptionResult;
+import org.eclipse.aether.repository.Authentication;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.MirrorSelector;
+import org.eclipse.aether.repository.ProxySelector;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.eclipse.aether.util.repository.ConservativeAuthenticationSelector;
+import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
+import org.eclipse.aether.util.repository.DefaultMirrorSelector;
+import org.eclipse.aether.util.repository.DefaultProxySelector;
+
+/**
+ * An encapsulation of settings read from a user's Maven settings.xml.
+ *
+ * @author Andy Wilkinson
+ * @see MavenSettingsReader
+ */
+public class MavenSettings {
+
+ private final boolean offline;
+
+ private final MirrorSelector mirrorSelector;
+
+ private final AuthenticationSelector authenticationSelector;
+
+ private final ProxySelector proxySelector;
+
+ private final String localRepository;
+
+ private final List activeProfiles;
+
+ /**
+ * Create a new {@link MavenSettings} instance.
+ * @param settings the source settings
+ * @param decryptedSettings the decrypted settings
+ */
+ public MavenSettings(Settings settings, SettingsDecryptionResult decryptedSettings) {
+ this.offline = settings.isOffline();
+ this.mirrorSelector = createMirrorSelector(settings);
+ this.authenticationSelector = createAuthenticationSelector(decryptedSettings);
+ this.proxySelector = createProxySelector(decryptedSettings);
+ this.localRepository = settings.getLocalRepository();
+ this.activeProfiles = determineActiveProfiles(settings);
+ }
+
+ private MirrorSelector createMirrorSelector(Settings settings) {
+ DefaultMirrorSelector selector = new DefaultMirrorSelector();
+ for (Mirror mirror : settings.getMirrors()) {
+ selector.add(mirror.getId(), mirror.getUrl(), mirror.getLayout(), false,
+ mirror.getMirrorOf(), mirror.getMirrorOfLayouts());
+ }
+ return selector;
+ }
+
+ private AuthenticationSelector createAuthenticationSelector(
+ SettingsDecryptionResult decryptedSettings) {
+ DefaultAuthenticationSelector selector = new DefaultAuthenticationSelector();
+ for (Server server : decryptedSettings.getServers()) {
+ AuthenticationBuilder auth = new AuthenticationBuilder();
+ auth.addUsername(server.getUsername()).addPassword(server.getPassword());
+ auth.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
+ selector.add(server.getId(), auth.build());
+ }
+ return new ConservativeAuthenticationSelector(selector);
+ }
+
+ private ProxySelector createProxySelector(
+ SettingsDecryptionResult decryptedSettings) {
+ DefaultProxySelector selector = new DefaultProxySelector();
+ for (Proxy proxy : decryptedSettings.getProxies()) {
+ Authentication authentication = new AuthenticationBuilder()
+ .addUsername(proxy.getUsername()).addPassword(proxy.getPassword())
+ .build();
+ selector.add(
+ new org.eclipse.aether.repository.Proxy(proxy.getProtocol(),
+ proxy.getHost(), proxy.getPort(), authentication),
+ proxy.getNonProxyHosts());
+ }
+ return selector;
+ }
+
+ private List determineActiveProfiles(Settings settings) {
+ SpringBootCliModelProblemCollector problemCollector = new SpringBootCliModelProblemCollector();
+ List activeModelProfiles = createProfileSelector()
+ .getActiveProfiles(createModelProfiles(settings.getProfiles()),
+ new SpringBootCliProfileActivationContext(
+ settings.getActiveProfiles()),
+ problemCollector);
+ if (!problemCollector.getProblems().isEmpty()) {
+ throw new IllegalStateException(createFailureMessage(problemCollector));
+ }
+ List activeProfiles = new ArrayList();
+ Map profiles = settings.getProfilesAsMap();
+ for (org.apache.maven.model.Profile modelProfile : activeModelProfiles) {
+ activeProfiles.add(profiles.get(modelProfile.getId()));
+ }
+ return activeProfiles;
+ }
+
+ private String createFailureMessage(
+ SpringBootCliModelProblemCollector problemCollector) {
+ StringWriter message = new StringWriter();
+ PrintWriter printer = new PrintWriter(message);
+ printer.println("Failed to determine active profiles:");
+ for (ModelProblemCollectorRequest problem : problemCollector.getProblems()) {
+ printer.println(" " + problem.getMessage() + (problem.getLocation() != null
+ ? " at " + problem.getLocation() : ""));
+ if (problem.getException() != null) {
+ printer.println(indentStackTrace(problem.getException(), " "));
+ }
+ }
+ return message.toString();
+ }
+
+ private String indentStackTrace(Exception ex, String indent) {
+ return indentLines(printStackTrace(ex), indent);
+ }
+
+ private String printStackTrace(Exception ex) {
+ StringWriter stackTrace = new StringWriter();
+ PrintWriter printer = new PrintWriter(stackTrace);
+ ex.printStackTrace(printer);
+ return stackTrace.toString();
+ }
+
+ private String indentLines(String input, String indent) {
+ StringWriter indented = new StringWriter();
+ PrintWriter writer = new PrintWriter(indented);
+ String line;
+ BufferedReader reader = new BufferedReader(new StringReader(input));
+ try {
+ while ((line = reader.readLine()) != null) {
+ writer.println(indent + line);
+ }
+ }
+ catch (IOException ex) {
+ return input;
+ }
+ return indented.toString();
+ }
+
+ private DefaultProfileSelector createProfileSelector() {
+ DefaultProfileSelector selector = new DefaultProfileSelector();
+
+ selector.addProfileActivator(new FileProfileActivator()
+ .setPathTranslator(new DefaultPathTranslator()));
+ selector.addProfileActivator(new JdkVersionProfileActivator());
+ selector.addProfileActivator(new PropertyProfileActivator());
+ selector.addProfileActivator(new OperatingSystemProfileActivator());
+ return selector;
+ }
+
+ private List createModelProfiles(
+ List profiles) {
+ List modelProfiles = new ArrayList();
+ for (Profile profile : profiles) {
+ org.apache.maven.model.Profile modelProfile = new org.apache.maven.model.Profile();
+ modelProfile.setId(profile.getId());
+ if (profile.getActivation() != null) {
+ modelProfile
+ .setActivation(createModelActivation(profile.getActivation()));
+ }
+ modelProfiles.add(modelProfile);
+ }
+ return modelProfiles;
+ }
+
+ private org.apache.maven.model.Activation createModelActivation(
+ Activation activation) {
+ org.apache.maven.model.Activation modelActivation = new org.apache.maven.model.Activation();
+ modelActivation.setActiveByDefault(activation.isActiveByDefault());
+ if (activation.getFile() != null) {
+ ActivationFile activationFile = new ActivationFile();
+ activationFile.setExists(activation.getFile().getExists());
+ activationFile.setMissing(activation.getFile().getMissing());
+ modelActivation.setFile(activationFile);
+ }
+ modelActivation.setJdk(activation.getJdk());
+ if (activation.getOs() != null) {
+ ActivationOS os = new ActivationOS();
+ os.setArch(activation.getOs().getArch());
+ os.setFamily(activation.getOs().getFamily());
+ os.setName(activation.getOs().getName());
+ os.setVersion(activation.getOs().getVersion());
+ modelActivation.setOs(os);
+ }
+ if (activation.getProperty() != null) {
+ ActivationProperty property = new ActivationProperty();
+ property.setName(activation.getProperty().getName());
+ property.setValue(activation.getProperty().getValue());
+ modelActivation.setProperty(property);
+ }
+ return modelActivation;
+ }
+
+ public boolean getOffline() {
+ return this.offline;
+ }
+
+ public MirrorSelector getMirrorSelector() {
+ return this.mirrorSelector;
+ }
+
+ public AuthenticationSelector getAuthenticationSelector() {
+ return this.authenticationSelector;
+ }
+
+ public ProxySelector getProxySelector() {
+ return this.proxySelector;
+ }
+
+ public String getLocalRepository() {
+ return this.localRepository;
+ }
+
+ public List getActiveProfiles() {
+ return this.activeProfiles;
+ }
+
+ private static final class SpringBootCliProfileActivationContext
+ implements ProfileActivationContext {
+
+ private final List activeProfiles;
+
+ SpringBootCliProfileActivationContext(List activeProfiles) {
+ this.activeProfiles = activeProfiles;
+ }
+
+ @Override
+ public List getActiveProfileIds() {
+ return this.activeProfiles;
+ }
+
+ @Override
+ public List getInactiveProfileIds() {
+ return Collections.emptyList();
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ public Map getSystemProperties() {
+ return (Map) System.getProperties();
+ }
+
+ @Override
+ public Map getUserProperties() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public File getProjectDirectory() {
+ return new File(".");
+ }
+
+ @Override
+ public Map getProjectProperties() {
+ return Collections.emptyMap();
+ }
+
+ }
+
+ private static final class SpringBootCliModelProblemCollector
+ implements ModelProblemCollector {
+
+ private final List problems = new ArrayList();
+
+ @Override
+ public void add(ModelProblemCollectorRequest req) {
+ this.problems.add(req);
+ }
+
+ List getProblems() {
+ return this.problems;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java
new file mode 100644
index 000000000..fafeb14d3
--- /dev/null
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2012-2017 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.function.compiler.java;
+
+import java.io.File;
+import java.lang.reflect.Field;
+
+import org.apache.maven.settings.Settings;
+import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
+import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
+import org.apache.maven.settings.building.SettingsBuildingException;
+import org.apache.maven.settings.building.SettingsBuildingRequest;
+import org.apache.maven.settings.crypto.DefaultSettingsDecrypter;
+import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
+import org.apache.maven.settings.crypto.SettingsDecrypter;
+import org.apache.maven.settings.crypto.SettingsDecryptionResult;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonatype.plexus.components.cipher.DefaultPlexusCipher;
+import org.sonatype.plexus.components.cipher.PlexusCipherException;
+import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
+
+/**
+ * {@code MavenSettingsReader} reads settings from a user's Maven settings.xml file,
+ * decrypting them if necessary using settings-security.xml.
+ *
+ * @author Andy Wilkinson
+ */
+public class MavenSettingsReader {
+
+ private static final Logger log = LoggerFactory.getLogger(MavenSettingsReader.class);
+
+ private final String homeDir;
+
+ public MavenSettingsReader() {
+ this(System.getProperty("user.home"));
+ }
+
+ public MavenSettingsReader(String homeDir) {
+ this.homeDir = homeDir;
+ }
+
+ public MavenSettings readSettings() {
+ Settings settings = loadSettings();
+ SettingsDecryptionResult decrypted = decryptSettings(settings);
+ if (!decrypted.getProblems().isEmpty()) {
+ log.error(
+ "Maven settings decryption failed. Some Maven repositories may be inaccessible");
+ // Continue - the encrypted credentials may not be used
+ }
+ return new MavenSettings(settings, decrypted);
+ }
+
+ public static void applySettings(MavenSettings settings,
+ DefaultRepositorySystemSession session) {
+ if (settings.getLocalRepository() != null) {
+ try {
+ session.setLocalRepositoryManager(
+ new SimpleLocalRepositoryManagerFactory().newInstance(session,
+ new LocalRepository(settings.getLocalRepository())));
+ }
+ catch (NoLocalRepositoryManagerException e) {
+ throw new IllegalStateException(
+ "Cannot set local repository to " + settings.getLocalRepository(),
+ e);
+ }
+ }
+ session.setOffline(settings.getOffline());
+ session.setMirrorSelector(settings.getMirrorSelector());
+ session.setAuthenticationSelector(settings.getAuthenticationSelector());
+ session.setProxySelector(settings.getProxySelector());
+ }
+
+ private Settings loadSettings() {
+ File settingsFile = new File(this.homeDir, ".m2/settings.xml");
+ if (settingsFile.exists()) {
+ log.info("Reading settings from: " + settingsFile);
+ }
+ else {
+ log.info("No settings found at: " + settingsFile);
+ }
+ SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
+ request.setUserSettingsFile(settingsFile);
+ request.setSystemProperties(System.getProperties());
+ try {
+ return new DefaultSettingsBuilderFactory().newInstance().build(request)
+ .getEffectiveSettings();
+ }
+ catch (SettingsBuildingException ex) {
+ throw new IllegalStateException(
+ "Failed to build settings from " + settingsFile, ex);
+ }
+ }
+
+ private SettingsDecryptionResult decryptSettings(Settings settings) {
+ DefaultSettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(
+ settings);
+
+ return createSettingsDecrypter().decrypt(request);
+ }
+
+ private SettingsDecrypter createSettingsDecrypter() {
+ SettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter();
+ setField(DefaultSettingsDecrypter.class, "securityDispatcher", settingsDecrypter,
+ new SpringBootSecDispatcher());
+ return settingsDecrypter;
+ }
+
+ private void setField(Class> sourceClass, String fieldName, Object target,
+ Object value) {
+ try {
+ Field field = sourceClass.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(
+ "Failed to set field '" + fieldName + "' on '" + target + "'", ex);
+ }
+ }
+
+ private class SpringBootSecDispatcher extends DefaultSecDispatcher {
+
+ private static final String SECURITY_XML = ".m2/settings-security.xml";
+
+ SpringBootSecDispatcher() {
+ File file = new File(MavenSettingsReader.this.homeDir, SECURITY_XML);
+ this._configurationFile = file.getAbsolutePath();
+ try {
+ this._cipher = new DefaultPlexusCipher();
+ }
+ catch (PlexusCipherException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ }
+
+}
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java
index ad30ca0a4..9e6663df7 100644
--- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java
@@ -22,7 +22,9 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.tools.FileObject;
@@ -31,6 +33,8 @@ import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardLocation;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.Dependency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,6 +54,8 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
private List toClose = new ArrayList<>();
+ private Map resolvedAdditionalDependencies = new LinkedHashMap<>();
+
public MemoryBasedJavaFileManager() {
outputCollector = new CompilationOutputCollector();
}
@@ -84,6 +90,11 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
else if (location == StandardLocation.CLASS_PATH
&& (kinds == null || kinds.contains(Kind.CLASS))) {
String javaClassPath = getClassPath();
+ if (!resolvedAdditionalDependencies.isEmpty()) {
+ for (File resolvedAdditionalDependency: resolvedAdditionalDependencies.values()) {
+ javaClassPath += File.pathSeparatorChar + resolvedAdditionalDependency.toURI().toString().substring("file:".length());
+ }
+ }
logger.debug("Creating iterable for class path: {}", javaClassPath);
resultIterable = new IterableClasspath(javaClassPath, packageName, recurse);
toClose.add(resultIterable);
@@ -203,4 +214,35 @@ public class MemoryBasedJavaFileManager implements JavaFileManager {
return outputCollector.getCompiledClasses();
}
+ public List addAndResolveDependencies(String[] dependencies) {
+ List resolutionMessages = new ArrayList<>();
+ for (String dependency: dependencies) {
+ if (dependency.startsWith("maven:")) {
+ // Resolving an explicit external archive
+ String coordinates = dependency.replaceFirst("maven:\\/*", "");
+ DependencyResolver engine = DependencyResolver.instance();
+ try {
+ File resolved = engine.resolve(new Dependency(new DefaultArtifact(coordinates), "runtime"));
+ // Example:
+ // dependency = maven://org.springframework:spring-expression:4.3.9.RELEASE
+ // resolved.toURI() = file:/Users/aclement/.m2/repository/org/springframework/spring-expression/4.3.9.RELEASE/spring-expression-4.3.9.RELEASE.jar
+ resolvedAdditionalDependencies.put(dependency, resolved);
+ } catch (RuntimeException re) {
+ CompilationMessage compilationMessage =
+ new CompilationMessage(CompilationMessage.Kind.ERROR,re.getMessage(),null,0,0);
+ resolutionMessages.add(compilationMessage);
+ }
+ }
+ else {
+ resolutionMessages.add(new CompilationMessage(CompilationMessage.Kind.ERROR,
+ "Unrecognized dependency: "+dependency+" (expected something of the form: maven://groupId:artifactId:version)",null,0,0));
+ }
+ }
+ return resolutionMessages;
+ }
+
+ public Map getResolvedAdditionalDependencies() {
+ return resolvedAdditionalDependencies;
+ }
+
}
\ No newline at end of file
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java
index 234bf99b7..b8cb44b39 100644
--- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java
@@ -38,7 +38,7 @@ import org.slf4j.LoggerFactory;
* @author Andy Clement
*/
public class RuntimeJavaCompiler {
-
+
private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
private static Logger logger = LoggerFactory.getLogger(RuntimeJavaCompiler.class);
@@ -49,13 +49,15 @@ public class RuntimeJavaCompiler {
* classes.
* @param className the name of the class (dotted form, e.g. com.foo.bar.Goo)
* @param classSourceCode the full source code for the class
+ * @param dependencies optional maven coordinates for dependencies "maven://groupId:artifactId:version"
* @return a CompilationResult that encapsulates what happened during compilation (classes/messages produced)
*/
- public CompilationResult compile(String className, String classSourceCode) {
+ public CompilationResult compile(String className, String classSourceCode, String... dependencies) {
logger.info("Compiling source for class {} using compiler {}",className,compiler.getClass().getName());
DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
MemoryBasedJavaFileManager fileManager = new MemoryBasedJavaFileManager();
+ List resolutionMessages = fileManager.addAndResolveDependencies(dependencies);
// JavaFileObject sourceFile = new StringBasedJavaSourceFileObject(className, classSourceCode);
JavaFileObject sourceFile = InMemoryJavaFileObject.getSourceJavaFileObject(className, classSourceCode);
// new InMemoryJavaFileObject(StandardLocation.SOURCE_PATH, className, javax.tools.JavaFileObject.Kind.SOURCE, null);
@@ -69,6 +71,8 @@ public class RuntimeJavaCompiler {
boolean success = task.call();
CompilationResult compilationResult = new CompilationResult(success);
+ compilationResult.recordCompilationMessages(resolutionMessages);
+ compilationResult.setResolvedAdditionalDependencies(new ArrayList<>(fileManager.getResolvedAdditionalDependencies().values()));
// If successful there may be no errors but there might be info/warnings
for (Diagnostic extends JavaFileObject> diagnostic : diagnosticCollector.getDiagnostics()) {
diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java
index 221c1c6fb..dbc16a062 100644
--- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java
+++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java
@@ -16,8 +16,10 @@
package org.springframework.cloud.function.compiler.java;
+import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
+import java.util.List;
/**
* Very simple classloader that can be used to load the compiled types.
@@ -32,6 +34,22 @@ public class SimpleClassLoader extends URLClassLoader {
super(NO_URLS, classLoader);
}
+ public SimpleClassLoader(List resolvedAdditionalDependencies, ClassLoader classLoader) {
+ super(toUrls(resolvedAdditionalDependencies), classLoader);
+ }
+
+ private static URL[] toUrls(List resolvedAdditionalDependencies) {
+ URL[] urls = new URL[resolvedAdditionalDependencies.size()];
+ for (int i=0,max=resolvedAdditionalDependencies.size();i defineClass(String name, byte[] bytes) {
return super.defineClass(name, bytes, 0, bytes.length);
}
diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java
new file mode 100644
index 000000000..5809d406b
--- /dev/null
+++ b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2017 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.function.compiler.java;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+/**
+ * @author Andy Clement
+ */
+@SuppressWarnings("unchecked")
+public class RuntimeJavaCompilerTests {
+
+ @Test
+ public void basicCompilation() {
+ RuntimeJavaCompiler rjc = new RuntimeJavaCompiler();
+ CompilationResult cr = rjc.compile("A", "public class A {}");
+ List compilationMessages = cr.getCompilationMessages();
+ assertTrue(compilationMessages.isEmpty());
+ }
+
+ @Test
+ public void missingType() throws Exception {
+ RuntimeJavaCompiler rjc = new RuntimeJavaCompiler();
+ CompilationResult cr = rjc.compile("A",
+ "public class A implements java.util.function.Supplier { "+
+ " public String get() {\n"+
+ " ExpressionParser parser = new SpelExpressionParser();\n" +
+ " Expression exp = parser.parseExpression(\"'Hello World'\");\n" +
+ " String message = (String) exp.getValue();"+
+ " return message;\n"+
+ " }\n"+
+ "}");
+ List compilationMessages = cr.getCompilationMessages();
+ assertEquals(3,compilationMessages.size());
+ assertTrue(compilationMessages.get(0).getMessage().contains("cannot find symbol"));
+ assertTrue(compilationMessages.get(0).getMessage().contains("class ExpressionParser"));
+ assertTrue(compilationMessages.get(1).getMessage().contains("cannot find symbol"));
+ assertTrue(compilationMessages.get(1).getMessage().contains("class SpelExpressionParser"));
+ assertTrue(compilationMessages.get(2).getMessage().contains("cannot find symbol"));
+ assertTrue(compilationMessages.get(2).getMessage().contains("class Expression"));
+ }
+
+ @Test
+ public void okWithImportedDependencies() throws Exception {
+ RuntimeJavaCompiler rjc = new RuntimeJavaCompiler();
+ CompilationResult cr = rjc.compile("A",
+ "import org.springframework.expression.*;\n"+
+ "import org.springframework.expression.spel.standard.*;\n"+
+ "public class A implements java.util.function.Supplier {\n"+
+ " public String get() {\n"+
+ " ExpressionParser parser = new SpelExpressionParser();\n" +
+ " Expression exp = parser.parseExpression(\"'Hello World'\");\n" +
+ " String message = (String) exp.getValue();\n"+
+ " return message;\n"+
+ " }\n"+
+ "}","maven://org.springframework:spring-expression:4.3.9.RELEASE");
+ List compilationMessages = cr.getCompilationMessages();
+ assertTrue(compilationMessages.isEmpty());
+ try (SimpleClassLoader cl = new SimpleClassLoader(this.getClass().getClassLoader())) {
+ Class> clazz = cl.defineClass("A",cr.getClassBytes("A"));
+ Supplier supplier = (Supplier) clazz.newInstance();
+ assertEquals("Hello World",supplier.get());
+ }
+ }
+
+ @Test
+ public void okWithImportedDependencies2() throws Exception {
+ RuntimeJavaCompiler rjc = new RuntimeJavaCompiler();
+ String source =
+ "import org.joda.time.*;\n"+
+ "public class A implements java.util.function.Supplier {\n"+
+ " public String get() {\n"+
+ " DateTime dt = new DateTime();\n" +
+ " int month = dt.getMonthOfYear();\n"+
+ " return String.valueOf(month>0);\n"+
+ " }\n"+
+ "}";
+ CompilationResult cr = rjc.compile("A", source, "maven://joda-time:joda-time:2.9.9");
+ List compilationMessages = cr.getCompilationMessages();
+ assertTrue(compilationMessages.isEmpty());
+ List resolvedAdditionalDependencies = cr.getResolvedAdditionalDependencies();
+ try (SimpleClassLoader cl = new SimpleClassLoader(resolvedAdditionalDependencies, this.getClass().getClassLoader())) {
+ Class> clazz = cl.defineClass("A",cr.getClassBytes("A"));
+ Supplier supplier = (Supplier) clazz.newInstance();
+ assertEquals("true",supplier.get());
+ }
+
+ cr = rjc.compile("A", source,
+ "maven://org.springframework:spring-expression:4.3.9.RELEASE",
+ "maven://joda-time:joda-time:2.9.9");
+ compilationMessages = cr.getCompilationMessages();
+ assertTrue(compilationMessages.isEmpty());
+ resolvedAdditionalDependencies = cr.getResolvedAdditionalDependencies();
+ try (SimpleClassLoader cl = new SimpleClassLoader(resolvedAdditionalDependencies, this.getClass().getClassLoader())) {
+ Class> clazz = cl.defineClass("A",cr.getClassBytes("A"));
+ Supplier supplier = (Supplier) clazz.newInstance();
+ assertEquals("true",supplier.get());
+ }
+ }
+
+ @Test
+ public void dependencyResolution() throws Exception {
+ // Failure:
+ RuntimeJavaCompiler rjc = new RuntimeJavaCompiler();
+ CompilationResult cr = rjc.compile("A",
+ "public class A {}",
+ "maven://org.springframework:spring-expression2:4.3.9.RELEASE"); // extra '2' in there
+ List compilationMessages = cr.getCompilationMessages();
+ assertEquals(1,compilationMessages.size());
+ // ERROR:org.eclipse.aether.resolution.ArtifactResolutionException: Could not find artifact org.springframework:spring-expression2:jar:4.3.9.RELEASE in spring-snapshots (https://repo.spring.io/libs-snapshot)
+ assertTrue(compilationMessages.get(0).getMessage().contains("Could not find artifact org.springframework:spring-expression2:jar:4.3.9.RELEASE"));
+
+ // Failure:
+ rjc = new RuntimeJavaCompiler();
+ cr = rjc.compile("A",
+ "public class A {}",
+ "trouble://org.springframework:spring-expression:4.3.9.RELEASE"); // rogue prefix (should be "maven:")
+ compilationMessages = cr.getCompilationMessages();
+ assertEquals(1,compilationMessages.size());
+ assertTrue(compilationMessages.get(0).toString(),compilationMessages.get(0).getMessage().contains("Unrecognized dependency: "));
+
+ // Success
+ rjc = new RuntimeJavaCompiler();
+ cr = rjc.compile("A",
+ "public class A {}",
+ "maven://joda-time:joda-time:2.9.9");
+ compilationMessages = cr.getCompilationMessages();
+ assertEquals(0,compilationMessages.size());
+ List resolvedAdditionalDependencies = cr.getResolvedAdditionalDependencies();
+ assertEquals(1, resolvedAdditionalDependencies.size());
+ assertTrue("Expected this to end with 'joda-time-2.9.9.jar': "+resolvedAdditionalDependencies.get(0).toString(),
+ resolvedAdditionalDependencies.get(0).toString().endsWith("joda-time-2.9.9.jar"));
+ }
+
+}