Commit 96e10104 authored by Andy Wilkinson's avatar Andy Wilkinson

Add a command to produce a self-contained executable JAR for a CLI app

A new command, jar, has been added to the CLI. The command can be
used to create a self-contained executable JAR file from a CLI app.

Basic usage is:

spring jar <jar-name> <source-files>

For example:

spring jar my-app.jar *.groovy

The resulting jar will contain the classes generated by compiling the
source files, all of the application's dependencies, and entries
on the application's classpath.

By default a CLI application has the current working directory on
its classpath. This can be overridden using the --classpath option.
Any file that is referenced directly by the classpath is always
included in the jar. Any file that is found a result of being
contained within a directory that is on the classpath is subject to
filtering to determine whether or not it should be included. The
default includes are public/**, static/**, resources/**,
META-INF/**, *. The default excludes are .*, repository/**, build/**,
target/**. To be included in the jar, a file must match one of the
includes and none of the excludes. The filters can be overridden using
the --include and --exclude options.

Closes #241
parent a99a3896
......@@ -198,3 +198,18 @@ $ . ~/.gvm/springboot/current/bash_completion.d/spring
$ spring <HIT TAB HERE>
clean -d debug help run test version
```
## Packaging Your Application
You can use the `jar` command to package your application into a
self-contained executable jar file. For example:
```
$ spring jar my-app.jar *.groovy
```
The resulting jar will containe the classes produced by compiling
the application and all of the application's dependencies such that
it can then be run using `java -jar`. The jar file will also contain
entries from the application's classpath. See the output of
`spring help jar` for more information.
\ No newline at end of file
......@@ -33,6 +33,11 @@
<artifactId>spring-boot-dependency-tools</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-loader-tools</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jline</groupId>
<artifactId>jline</artifactId>
......@@ -98,6 +103,12 @@
<artifactId>aether-util</artifactId>
</dependency>
<!-- Provided -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-templates</artifactId>
......
......@@ -43,7 +43,7 @@ public class CommandLineIT {
Invocation cli = this.cli.invoke("hint");
assertThat(cli.await(), equalTo(0));
assertThat(cli.getErrorOutput().length(), equalTo(0));
assertThat(cli.getStandardOutputLines().size(), equalTo(6));
assertThat(cli.getStandardOutputLines().size(), equalTo(7));
}
@Test
......
/*
* Copyright 2012-2014 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.boot.cli;
import java.io.File;
import org.junit.Test;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Andy Wilkinson
*/
public class JarCommandIT {
private final CommandLineInvoker cli = new CommandLineInvoker(new File(
"src/it/resources/jar-command"));
@Test
public void noArguments() throws Exception {
Invocation invocation = this.cli.invoke("jar");
invocation.await();
assertEquals(0, invocation.getStandardOutput().length());
assertEquals(
"The name of the resulting jar and at least one source file must be specified",
invocation.getErrorOutput().trim());
}
@Test
public void noSources() throws Exception {
Invocation invocation = this.cli.invoke("jar", "test-app.jar");
invocation.await();
assertEquals(0, invocation.getStandardOutput().length());
assertEquals(
"The name of the resulting jar and at least one source file must be specified",
invocation.getErrorOutput().trim());
}
@Test
public void jarCreation() throws Exception {
File jar = new File("target/test-app.jar");
Invocation invocation = this.cli.invoke("jar", jar.getAbsolutePath(),
"jar.groovy");
invocation.await();
assertEquals(0, invocation.getErrorOutput().length());
assertTrue(jar.exists());
ProcessBuilder builder = new ProcessBuilder(System.getProperty("java.home")
+ "/bin/java", "-jar", jar.getAbsolutePath());
Process process = builder.start();
Invocation appInvocation = new Invocation(process);
appInvocation.await();
assertEquals(0, appInvocation.getErrorOutput().length());
assertTrue(appInvocation.getStandardOutput().contains("Hello World!"));
assertTrue(appInvocation.getStandardOutput().contains("/static/test.txt"));
}
}
......@@ -37,6 +37,16 @@ import org.springframework.util.Assert;
*/
public final class CommandLineInvoker {
private final File workingDirectory;
public CommandLineInvoker() {
this(new File("."));
}
public CommandLineInvoker(File workingDirectory) {
this.workingDirectory = workingDirectory;
}
public Invocation invoke(String... args) throws IOException {
return new Invocation(runCliProcess(args));
}
......@@ -45,7 +55,7 @@ public final class CommandLineInvoker {
List<String> command = new ArrayList<String>();
command.add(findLaunchScript().getAbsolutePath());
command.addAll(Arrays.asList(args));
return new ProcessBuilder(command).start();
return new ProcessBuilder(command).directory(this.workingDirectory).start();
}
private File findLaunchScript() {
......@@ -70,9 +80,9 @@ public final class CommandLineInvoker {
}
/**
* An ongoing CLI invocation.
* An ongoing Process invocation.
*/
public final class Invocation {
public static final class Invocation {
private final StringBuffer err = new StringBuffer();
......@@ -80,7 +90,7 @@ public final class CommandLineInvoker {
private final Process process;
Invocation(Process process) {
public Invocation(Process process) {
this.process = process;
new Thread(new StreamReadingRunnable(this.process.getErrorStream(), this.err))
.start();
......
package org.test
@Component
class Example implements CommandLineRunner {
@Autowired
private MyService myService
void run(String... args) {
println "Hello ${this.myService.sayWorld()}"
println getClass().getResource('/static/test.txt')
}
}
@Service
class MyService {
String sayWorld() {
return 'World!'
}
}
\ No newline at end of file
......@@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.boot.cli.command.core.VersionCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.boot.cli.command.jar.JarCommand;
import org.springframework.boot.cli.command.run.RunCommand;
import org.springframework.boot.cli.command.test.TestCommand;
......@@ -33,7 +34,8 @@ import org.springframework.boot.cli.command.test.TestCommand;
public class DefaultCommandFactory implements CommandFactory {
private static final List<Command> DEFAULT_COMMANDS = Arrays.<Command> asList(
new VersionCommand(), new RunCommand(), new TestCommand(), new GrabCommand());
new VersionCommand(), new RunCommand(), new TestCommand(), new GrabCommand(),
new JarCommand());
@Override
public Collection<Command> getCommands() {
......
......@@ -31,6 +31,7 @@ import org.springframework.util.Assert;
* @author Phillip Webb
* @author Dave Syer
* @author Greg Turnquist
* @author Andy Wilkinson
*/
public class SourceOptions {
......@@ -46,6 +47,14 @@ public class SourceOptions {
this(options, null);
}
/**
* Create a new {@link SourceOptions} instance.
* @param arguments the source arguments
*/
public SourceOptions(List<?> arguments) {
this(arguments, null);
}
/**
* Create a new {@link SourceOptions} instance. If it is an error to pass options that
* specify non-existent sources, but the default paths are allowed not to exist (the
......@@ -57,7 +66,10 @@ public class SourceOptions {
* found in the local filesystem
*/
public SourceOptions(OptionSet optionSet, ClassLoader classLoader) {
List<?> nonOptionArguments = optionSet.nonOptionArguments();
this(optionSet.nonOptionArguments(), classLoader);
}
private SourceOptions(List<?> nonOptionArguments, ClassLoader classLoader) {
List<String> sources = new ArrayList<String>();
int sourceArgCount = 0;
for (Object option : nonOptionArguments) {
......@@ -84,7 +96,7 @@ public class SourceOptions {
}
this.args = Collections.unmodifiableList(nonOptionArguments.subList(
sourceArgCount, nonOptionArguments.size()));
Assert.isTrue(sources.size() > 0, "Please specify at least one file to run");
Assert.isTrue(sources.size() > 0, "Please specify at least one file");
this.sources = Collections.unmodifiableList(sources);
}
......
/*
* Copyright 2012-2014 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.boot.cli.command.jar;
import groovy.lang.Grab;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.CompilerOptionHandler;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.SourceOptions;
import org.springframework.boot.cli.command.jar.ResourceMatcher.MatchedResource;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompiler.CompilationCallback;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.GroovyCompilerConfigurationAdapter;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.loader.ArchiveResolver;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.util.StringUtils;
/**
* {@link Command} to create a self-contained executable jar file from a CLI application
*
* @author Andy Wilkinson
*/
public class JarCommand extends OptionParsingCommand {
public JarCommand() {
super(
"jar",
"Create a self-contained executable jar file from a Spring Groovy script",
new JarOptionHandler());
}
@Override
public String getUsageHelp() {
return "[options] <jar-name> <files>";
}
private static final class JarOptionHandler extends CompilerOptionHandler {
private OptionSpec<String> includeOption;
private OptionSpec<String> excludeOption;
@Override
protected void doOptions() {
this.includeOption = option(
"include",
"Pattern applied to directories on the classpath to find files to include in the resulting jar")
.withRequiredArg().defaultsTo("public/**", "static/**",
"resources/**", "META-INF/**", "*");
this.excludeOption = option(
"exclude",
"Pattern applied to directories on the claspath to find files to exclude from the resulting jar")
.withRequiredArg().defaultsTo(".*", "repository/**", "build/**",
"target/**");
}
@Override
protected void run(OptionSet options) throws Exception {
List<?> nonOptionArguments = new ArrayList<Object>(
options.nonOptionArguments());
if (nonOptionArguments.size() < 2) {
throw new IllegalStateException(
"The name of the resulting jar and at least one source file must be specified");
}
File output = new File((String) nonOptionArguments.remove(0));
if (output.exists() && !output.delete()) {
throw new IllegalStateException(
"Failed to delete existing application jar file "
+ output.getPath());
}
GroovyCompiler groovyCompiler = createCompiler(options);
List<URL> classpathUrls = Arrays.asList(groovyCompiler.getLoader().getURLs());
List<MatchedResource> classpathEntries = findClasspathEntries(classpathUrls,
options);
final Map<String, byte[]> compiledClasses = new HashMap<String, byte[]>();
groovyCompiler.compile(new CompilationCallback() {
@Override
public void byteCodeGenerated(byte[] byteCode, ClassNode classNode)
throws IOException {
String className = classNode.getName();
compiledClasses.put(className, byteCode);
}
}, new SourceOptions(nonOptionArguments).getSourcesArray());
List<URL> dependencyUrls = new ArrayList<URL>(Arrays.asList(groovyCompiler
.getLoader().getURLs()));
dependencyUrls.removeAll(classpathUrls);
JarWriter jarWriter = new JarWriter(output);
try {
jarWriter.writeManifest(createManifest(compiledClasses));
addDependencies(jarWriter, dependencyUrls);
addClasspathEntries(jarWriter, classpathEntries);
addApplicationClasses(jarWriter, compiledClasses);
jarWriter.writeLoaderClasses();
addJarRunner(jarWriter);
}
finally {
jarWriter.close();
}
}
private GroovyCompiler createCompiler(OptionSet options) {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
GroovyCompilerConfiguration configuration = new GroovyCompilerConfigurationAdapter(
options, this, repositoryConfiguration);
GroovyCompiler groovyCompiler = new GroovyCompiler(configuration);
groovyCompiler.getAstTransformations().add(0, new ASTTransformation() {
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
ModuleNode module = (ModuleNode) node;
for (ClassNode classNode : module.getClasses()) {
AnnotationNode annotation = new AnnotationNode(
new ClassNode(Grab.class));
annotation.addMember("value", new ConstantExpression(
"groovy"));
classNode.addAnnotation(annotation);
}
}
}
}
});
return groovyCompiler;
}
private List<MatchedResource> findClasspathEntries(List<URL> classpath,
OptionSet options) throws IOException {
ResourceMatcher resourceCollector = new ResourceMatcher(
options.valuesOf(this.includeOption),
options.valuesOf(this.excludeOption));
List<File> roots = new ArrayList<File>();
for (URL classpathEntry : classpath) {
roots.add(new File(URI.create(classpathEntry.toString())));
}
return resourceCollector.matchResources(roots);
}
private Manifest createManifest(final Map<String, byte[]> compiledClasses) {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
manifest.getMainAttributes()
.putValue(
"Application-Classes",
StringUtils.collectionToCommaDelimitedString(compiledClasses
.keySet()));
manifest.getMainAttributes()
.putValue("Main-Class", JarRunner.class.getName());
return manifest;
}
private void addDependencies(JarWriter jarWriter, List<URL> urls)
throws IOException, URISyntaxException, FileNotFoundException {
for (URL url : urls) {
addDependency(jarWriter, new File(url.toURI()));
}
}
private void addDependency(JarWriter jarWriter, File dependency)
throws FileNotFoundException, IOException {
if (dependency.isFile()) {
jarWriter.writeNestedLibrary("lib/", dependency);
}
}
private void addClasspathEntries(JarWriter jarWriter,
List<MatchedResource> classpathEntries) throws IOException {
for (MatchedResource classpathEntry : classpathEntries) {
if (classpathEntry.isRoot()) {
addDependency(jarWriter, classpathEntry.getFile());
}
else {
jarWriter.writeEntry(classpathEntry.getPath(), new FileInputStream(
classpathEntry.getFile()));
}
}
}
private void addApplicationClasses(JarWriter jarWriter,
final Map<String, byte[]> compiledClasses) throws IOException {
for (Entry<String, byte[]> entry : compiledClasses.entrySet()) {
String className = entry.getKey().replace(".", "/") + ".class";
jarWriter.writeEntry(className,
new ByteArrayInputStream(entry.getValue()));
}
}
private void addJarRunner(JarWriter jar) throws IOException, URISyntaxException {
JarFile jarFile = getJarFile();
String namePrefix = JarRunner.class.getName().replace(".", "/");
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith(namePrefix)) {
jar.writeEntry(entry.getName(), jarFile.getInputStream(entry));
}
}
}
private JarFile getJarFile() throws URISyntaxException, IOException {
return new JarFile(
new ArchiveResolver().resolveArchiveLocation(JarCommand.class));
}
}
}
/*
* Copyright 2012-2014 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.boot.cli.command.jar;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.ArchiveResolver;
import org.springframework.boot.loader.AsciiBytes;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
/**
* A runner for a CLI application that has been compiled and packaged as a jar file
*
* @author Andy Wilkinson
*/
public class JarRunner {
private static final AsciiBytes LIB = new AsciiBytes("lib/");
public static void main(String[] args) throws URISyntaxException, IOException,
ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Archive archive = new ArchiveResolver().resolveArchive(JarRunner.class);
ClassLoader classLoader = createClassLoader(archive);
Class<?>[] classes = loadApplicationClasses(archive, classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
// Use reflection to load and call Spring
Class<?> application = classLoader
.loadClass("org.springframework.boot.SpringApplication");
Method method = application.getMethod("run", Object[].class, String[].class);
method.invoke(null, classes, args);
}
private static ClassLoader createClassLoader(Archive archive) throws IOException,
MalformedURLException {
List<Archive> nestedArchives = archive.getNestedArchives(new EntryFilter() {
@Override
public boolean matches(Entry entry) {
return entry.getName().startsWith(LIB);
}
});
List<URL> urls = new ArrayList<URL>();
urls.add(archive.getUrl());
for (Archive nestedArchive : nestedArchives) {
urls.add(nestedArchive.getUrl());
}
ClassLoader classLoader = new LaunchedURLClassLoader(urls.toArray(new URL[urls
.size()]), JarRunner.class.getClassLoader());
return classLoader;
}
private static Class<?>[] loadApplicationClasses(Archive archive,
ClassLoader classLoader) throws ClassNotFoundException, IOException {
String[] classNames = archive.getManifest().getMainAttributes()
.getValue("Application-Classes").split(",");
Class<?>[] classes = new Class<?>[classNames.length];
for (int i = 0; i < classNames.length; i++) {
Class<?> applicationClass = classLoader.loadClass(classNames[i]);
classes[i] = applicationClass;
}
return classes;
}
}
/*
* Copyright 2012-2014 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.boot.cli.command.jar;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
/**
* Used to match resources for inclusion in a CLI application's jar file
*
* @author Andy Wilkinson
*/
final class ResourceMatcher {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final List<String> includes;
private final List<String> excludes;
ResourceMatcher(List<String> includes, List<String> excludes) {
this.includes = includes;
this.excludes = excludes;
}
List<MatchedResource> matchResources(List<File> roots) throws IOException {
List<MatchedResource> matchedResources = new ArrayList<MatchedResource>();
for (File root : roots) {
if (root.isFile()) {
matchedResources.add(new MatchedResource(root));
}
else {
matchedResources.addAll(matchResources(root));
}
}
return matchedResources;
}
private List<MatchedResource> matchResources(File root) throws IOException {
List<MatchedResource> resources = new ArrayList<MatchedResource>();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
new ResourceCollectionResourceLoader(root));
for (String include : this.includes) {
Resource[] candidates = resolver.getResources(include);
for (Resource candidate : candidates) {
File file = candidate.getFile();
if (file.isFile()) {
MatchedResource matchedResource = new MatchedResource(root, file);
if (!isExcluded(matchedResource)) {
resources.add(matchedResource);
}
}
}
}
return resources;
}
private boolean isExcluded(MatchedResource matchedResource) {
for (String exclude : this.excludes) {
if (this.pathMatcher.match(exclude, matchedResource.getPath())) {
return true;
}
}
return false;
}
private static final class ResourceCollectionResourceLoader extends
DefaultResourceLoader {
private final File root;
ResourceCollectionResourceLoader(File root) throws MalformedURLException {
super(new URLClassLoader(new URL[] { root.toURI().toURL() }) {
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return findResources(name);
}
@Override
public URL getResource(String name) {
return findResource(name);
}
});
this.root = root;
}
@Override
protected Resource getResourceByPath(String path) {
return new FileSystemResource(new File(this.root, path));
}
}
static final class MatchedResource {
private final File file;
private final String path;
private final boolean root;
private MatchedResource(File resourceFile) {
this(resourceFile, resourceFile.getName(), true);
}
private MatchedResource(File root, File resourceFile) {
this(resourceFile, resourceFile.getAbsolutePath().substring(
root.getAbsolutePath().length() + 1), false);
}
private MatchedResource(File resourceFile, String path, boolean root) {
this.file = resourceFile;
this.path = path;
this.root = root;
}
File getFile() {
return this.file;
}
String getPath() {
return this.path;
}
boolean isRoot() {
return this.root;
}
@Override
public String toString() {
return this.file.getAbsolutePath();
}
}
}
......@@ -34,6 +34,7 @@ import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilationUnit.ClassgenCallback;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases;
......@@ -42,6 +43,8 @@ import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngine;
import org.springframework.boot.cli.compiler.grape.AetherGrapeEngineFactory;
import org.springframework.boot.cli.compiler.grape.GrapeEngineInstaller;
......@@ -117,6 +120,10 @@ public class GroovyCompiler {
}
}
public List<ASTTransformation> getAstTransformations() {
return this.transformations;
}
public ExtendedGroovyClassLoader getLoader() {
return this.loader;
}
......@@ -203,6 +210,42 @@ public class GroovyCompiler {
return classes.toArray(new Class<?>[classes.size()]);
}
public void compile(final CompilationCallback callback, String... sources)
throws CompilationFailedException, IOException {
this.loader.clearCache();
CompilerConfiguration configuration = this.loader.getConfiguration();
final CompilationUnit compilationUnit = new CompilationUnit(configuration, null,
this.loader);
ClassgenCallback classgenCallback = new ClassgenCallback() {
@Override
public void call(ClassVisitor writer, ClassNode node)
throws CompilationFailedException {
try {
callback.byteCodeGenerated(((ClassWriter) writer).toByteArray(), node);
}
catch (IOException ioe) {
throw new CompilationFailedException(Phases.CLASS_GENERATION,
compilationUnit);
}
}
};
compilationUnit.setClassgenCallback(classgenCallback);
for (String source : sources) {
List<String> paths = ResourceUtils.getUrls(source, this.loader);
for (String path : paths) {
compilationUnit.addSource(new URL(path));
}
}
addAstTransformations(compilationUnit);
compilationUnit.compile(Phases.CLASS_GENERATION);
}
@SuppressWarnings("rawtypes")
private void addAstTransformations(CompilationUnit compilationUnit) {
LinkedList[] phaseOperations = getPhaseOperations(compilationUnit);
......@@ -286,4 +329,10 @@ public class GroovyCompiler {
}
public static interface CompilationCallback {
public void byteCodeGenerated(byte[] byteCode, ClassNode classNode)
throws IOException;
}
}
/*
* Copyright 2012-2014 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.boot.cli.command.jar;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.springframework.boot.cli.command.jar.ResourceMatcher.MatchedResource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author awilkinson
*/
public class ResourceMatcherTests {
private final ResourceMatcher resourceMatcher = new ResourceMatcher(Arrays.asList(
"alpha/**", "bravo/*", "*"), Arrays.asList(".*", "alpha/**/excluded"));
@Test
public void nonExistentRoot() throws IOException {
List<MatchedResource> matchedResources = this.resourceMatcher
.matchResources(Arrays.asList(new File("does-not-exist")));
assertEquals(0, matchedResources.size());
}
@Test
public void resourceMatching() throws IOException {
List<MatchedResource> matchedResources = this.resourceMatcher
.matchResources(Arrays.asList(new File(
"src/test/resources/resource-matcher/one"), new File(
"src/test/resources/resource-matcher/two"), new File(
"src/test/resources/resource-matcher/three")));
System.out.println(matchedResources);
List<String> paths = new ArrayList<String>();
for (MatchedResource resource : matchedResources) {
paths.add(resource.getPath());
}
assertEquals(6, paths.size());
assertTrue(paths.containsAll(Arrays.asList("alpha/nested/fileA", "bravo/fileC",
"fileD", "bravo/fileE", "fileF", "three")));
}
}
......@@ -45,7 +45,7 @@ import java.util.zip.ZipEntry;
*
* @author Phillip Webb
*/
class JarWriter {
public class JarWriter {
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
......@@ -107,6 +107,18 @@ class JarWriter {
}
}
/**
* Writes an entry. The {@code inputStream} is closed once the entry has been written
*
* @param entryName The name of the entry
* @param inputStream The stream from which the entry's data can be read
* @throws IOException if the write fails
*/
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
JarEntry entry = new JarEntry(entryName);
writeEntry(entry, new InputStreamEntryWriter(inputStream, true));
}
/**
* Write a nested library.
* @param destination the destination of the library
......
/*
* Copyright 2012-2014 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.boot.loader;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
/**
* Resolves the {@link Archive} from which a {@link Class} was loaded.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class ArchiveResolver {
/**
* Resolves the {@link Archive} that contains the given {@code clazz}.
* @param clazz The class whose containing archive is to be resolved
*
* @return The class's containing archive
* @throws IOException if an error occurs when resolving the containing archive
*/
public Archive resolveArchive(Class<?> clazz) throws IOException {
File root = resolveArchiveLocation(clazz);
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
/**
* Resolves the location of the archive that contains the given {@code clazz}.
* @param clazz The class for which the location of the containing archive is to be
* resolved
*
* @return The location of the class's containing archive
* @throws IOException if an error occurs when resolving the containing archive's
* location
*/
public File resolveArchiveLocation(Class<?> clazz) throws IOException {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
if (codeSource != null) {
File root;
URL location = codeSource.getLocation();
URLConnection connection = location.openConnection();
if (connection instanceof JarURLConnection) {
root = new File(((JarURLConnection) connection).getJarFile().getName());
}
else {
root = new File(location.getPath());
}
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return root;
}
throw new IllegalStateException("Unable to determine code source archive");
}
}
......@@ -16,10 +16,6 @@
package org.springframework.boot.loader;
import java.io.File;
import java.net.URI;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
......@@ -27,8 +23,6 @@ import java.util.jar.JarEntry;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
/**
* Base class for executable archive {@link Launcher}s.
......@@ -49,19 +43,7 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
}
private Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getPath());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
return new ArchiveResolver().resolveArchive(getClass());
}
protected final Archive getArchive() {
......
......@@ -21,13 +21,10 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
......@@ -497,19 +494,7 @@ public class PropertiesLauncher extends Launcher {
}
private Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getPath());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
return new ArchiveResolver().resolveArchive(getClass());
}
private void addParentClassLoaderEntries(List<Archive> lib) throws IOException,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment