/*
* 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Manifest;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
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.OptionParsingCommand;
import org.springframework.boot.cli.command.jar.ResourceMatcher.MatchedResource;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionSetGroovyCompilerConfiguration;
import org.springframework.boot.cli.command.options.SourceOptions;
import org.springframework.boot.cli.command.status.ExitStatus;
import org.springframework.boot.cli.compiler.GroovyCompiler;
import org.springframework.boot.cli.compiler.GroovyCompilerConfiguration;
import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.cli.jar.PackagedSpringApplicationLauncher;
import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Layouts;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
/**
* {@link Command} to create a self-contained executable jar file from a CLI application
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class JarCommand extends OptionParsingCommand {
private static final Layout LAYOUT = new Layouts.Jar();
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().withValuesSeparatedBy(",").defaultsTo("");
this.excludeOption = option(
"exclude",
"Pattern applied to directories on the claspath to find files to exclude from the resulting jar")
.withRequiredArg().withValuesSeparatedBy(",").defaultsTo("");
}
@Override
protected ExitStatus run(OptionSet options) throws Exception {
List<?> nonOptionArguments = new ArrayList<Object>(
options.nonOptionArguments());
Assert.isTrue(nonOptionArguments.size() >= 2,
"The name of the resulting jar and at least one source file must be specified");
File output = new File((String) nonOptionArguments.remove(0));
Assert.isTrue(output.getName().toLowerCase().endsWith(".jar"), "The output '"
+ output + "' is not a JAR file.");
deleteIfExists(output);
GroovyCompiler compiler = createCompiler(options);
List<URL> classpath = getClassPathUrls(compiler);
List<MatchedResource> classpathEntries = findMatchingClasspathEntries(
classpath, options);
String[] sources = new SourceOptions(nonOptionArguments).getSourcesArray();
Class<?>[] compiledClasses = compiler.compile(sources);
List<URL> dependencies = getClassPathUrls(compiler);
dependencies.removeAll(classpath);
writeJar(output, compiledClasses, classpathEntries, dependencies);
return ExitStatus.OK;
}
private void deleteIfExists(File file) {
if (file.exists() && !file.delete()) {
throw new IllegalStateException("Failed to delete existing file "
+ file.getPath());
}
}
private GroovyCompiler createCompiler(OptionSet options) {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
GroovyCompilerConfiguration configuration = new OptionSetGroovyCompilerConfiguration(
options, this, repositoryConfiguration);
GroovyCompiler groovyCompiler = new GroovyCompiler(configuration);
groovyCompiler.getAstTransformations().add(0, new GrabAnnotationTransform());
return groovyCompiler;
}
private List<URL> getClassPathUrls(GroovyCompiler compiler) {
return new ArrayList<URL>(Arrays.asList(compiler.getLoader().getURLs()));
}
private List<MatchedResource> findMatchingClasspathEntries(List<URL> classpath,
OptionSet options) throws IOException {
ResourceMatcher matcher = 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 matcher.find(roots);
}
private void writeJar(File file, Class<?>[] compiledClasses,
List<MatchedResource> classpathEntries, List<URL> dependencies)
throws FileNotFoundException, IOException, URISyntaxException {
JarWriter writer = new JarWriter(file);
try {
addManifest(writer, compiledClasses);
addCliClasses(writer);
for (Class<?> compiledClass : compiledClasses) {
addClass(writer, compiledClass);
}
addClasspathEntries(writer, classpathEntries);
addDependencies(writer, dependencies);
writer.writeLoaderClasses();
}
finally {
writer.close();
}
}
private void addManifest(JarWriter writer, Class<?>[] compiledClasses)
throws IOException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
manifest.getMainAttributes().putValue("Main-Class",
LAYOUT.getLauncherClassName());
manifest.getMainAttributes().putValue("Start-Class",
PackagedSpringApplicationLauncher.class.getName());
manifest.getMainAttributes().putValue(
PackagedSpringApplicationLauncher.SOURCE_ENTRY,
commaDelimitedClassNames(compiledClasses));
writer.writeManifest(manifest);
}
private String commaDelimitedClassNames(Class<?>[] classes) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < classes.length; i++) {
builder.append(i == 0 ? "" : ",");
builder.append(classes[i].getName());
}
return builder.toString();
}
private void addCliClasses(JarWriter writer) throws IOException {
addClass(writer, PackagedSpringApplicationLauncher.class);
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("org/springframework/boot/groovy/**");
for (Resource resource : resources) {
String url = resource.getURL().toString();
addResource(writer, resource,
url.substring(url.indexOf("org/springframework/boot/groovy/")));
}
}
private void addClass(JarWriter writer, Class<?> sourceClass) throws IOException {
String name = sourceClass.getName().replace(".", "/") + ".class";
InputStream stream = sourceClass.getResourceAsStream("/" + name);
writer.writeEntry(name, stream);
}
private void addResource(JarWriter writer, Resource resource, String name)
throws IOException {
InputStream stream = resource.getInputStream();
writer.writeEntry(name, stream);
}
private void addClasspathEntries(JarWriter writer, List<MatchedResource> entries)
throws IOException {
for (MatchedResource entry : entries) {
if (entry.isRoot()) {
addDependency(writer, entry.getFile());
}
else {
writer.writeEntry(entry.getName(),
new FileInputStream(entry.getFile()));
}
}
}
private void addDependencies(JarWriter writer, List<URL> urls)
throws IOException, URISyntaxException, FileNotFoundException {
for (URL url : urls) {
addDependency(writer, new File(url.toURI()));
}
}
private void addDependency(JarWriter writer, File dependency)
throws FileNotFoundException, IOException {
if (dependency.isFile()) {
writer.writeNestedLibrary("lib/", new Library(dependency,
LibraryScope.COMPILE));
}
}
}
/**
* {@link ASTTransformation} to change {@code @Grab} annotation values.
*/
private static class GrabAnnotationTransform implements ASTTransformation {
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
for (ASTNode node : nodes) {
if (node instanceof ModuleNode) {
visitModule((ModuleNode) node);
}
}
}
private void visitModule(ModuleNode module) {
for (ClassNode classNode : module.getClasses()) {
AnnotationNode annotation = new AnnotationNode(new ClassNode(Grab.class));
annotation.addMember("value", new ConstantExpression("groovy"));
classNode.addAnnotation(annotation);
// We only need to do it at most once
break;
}
// Remove GrabReolvers because all the dependencies are local now
removeGrabResolver(module.getClasses());
removeGrabResolver(module.getImports());
}
private void removeGrabResolver(List<? extends AnnotatedNode> nodes) {
for (AnnotatedNode classNode : nodes) {
List<AnnotationNode> annotations = classNode.getAnnotations();
for (AnnotationNode node : new ArrayList<AnnotationNode>(annotations)) {
if (node.getClassNode().getNameWithoutPackage()
.equals("GrabResolver")) {
annotations.remove(node);
}
}
}
}
}
}