From 2fdfda941676d438c6b1e012b3dd35acdd5069a6 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Tue, 4 Jul 2017 16:44:20 -0700 Subject: [PATCH] Allow dependencies to be specified when compiling, using maven coords --- spring-cloud-function-compiler/pom.xml | 25 + .../compiler/java/CompilationResult.java | 19 +- .../compiler/java/CompositeProxySelector.java | 50 ++ .../compiler/java/DependencyResolver.java | 477 ++++++++++++++++++ .../function/compiler/java/MavenSettings.java | 325 ++++++++++++ .../compiler/java/MavenSettingsReader.java | 157 ++++++ .../java/MemoryBasedJavaFileManager.java | 42 ++ .../compiler/java/RuntimeJavaCompiler.java | 8 +- .../compiler/java/SimpleClassLoader.java | 18 + .../java/RuntimeJavaCompilerTests.java | 156 ++++++ 10 files changed, 1273 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java create mode 100644 spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java 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 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 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")); + } + +}