/*
* 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.boot.cli.command.archive;
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 groovy.lang.Grab;
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.app.SpringApplicationLauncher;
import org.springframework.boot.cli.archive.PackagedSpringApplicationLauncher;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionParsingCommand;
import org.springframework.boot.cli.command.archive.ResourceMatcher.MatchedResource;
import org.springframework.boot.cli.command.options.CompilerOptionHandler;
import org.springframework.boot.cli.command.options.OptionHandler;
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.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.boot.loader.tools.Repackager;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
/**
* Abstract {@link Command} to create a self-contained executable archive file from a CLI
* application.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Andrey Stolyarov
* @author Henri Kerola
*/
abstract class ArchiveCommand extends OptionParsingCommand {
protected ArchiveCommand(String name, String description,
OptionHandler optionHandler) {
super(name, description, optionHandler);
}
@Override
public String getUsageHelp() {
return "[options] <" + getName() + "-name> <files>";
}
/**
* Abstract base {@link CompilerOptionHandler} for archive commands.
*/
protected abstract static class ArchiveOptionHandler extends CompilerOptionHandler {
private final String type;
private final Layout layout;
private OptionSpec<String> includeOption;
private OptionSpec<String> excludeOption;
public ArchiveOptionHandler(String type, Layout layout) {
this.type = type;
this.layout = layout;
}
protected Layout getLayout() {
return this.layout;
}
@Override
protected void doOptions() {
this.includeOption = option("include",
"Pattern applied to directories on the classpath to find files to "
+ "include in the resulting ").withRequiredArg()
.withValuesSeparatedBy(",").defaultsTo("");
this.excludeOption = option("exclude",
"Pattern applied to directories on the classpath to find files to "
+ "exclude from the resulting " + this.type).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 "
+ this.type + " and at least one source file must be specified");
File output = new File((String) nonOptionArguments.remove(0));
Assert.isTrue(output.getName().toLowerCase().endsWith("." + this.type),
"The output '" + output + "' is not a " + this.type.toUpperCase()
+ " 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<>(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<>();
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 {
final List<Library> libraries;
JarWriter writer = new JarWriter(file);
try {
addManifest(writer, compiledClasses);
addCliClasses(writer);
for (Class<?> compiledClass : compiledClasses) {
addClass(writer, compiledClass);
}
libraries = addClasspathEntries(writer, classpathEntries);
}
finally {
writer.close();
}
libraries.addAll(createLibraries(dependencies));
Repackager repackager = new Repackager(file);
repackager.setMainClass(PackagedSpringApplicationLauncher.class.getName());
repackager.repackage(new Libraries() {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
for (Library library : libraries) {
callback.library(library);
}
}
});
}
private List<Library> createLibraries(List<URL> dependencies)
throws URISyntaxException {
List<Library> libraries = new ArrayList<>();
for (URL dependency : dependencies) {
File file = new File(dependency.toURI());
libraries.add(new Library(file, getLibraryScope(file)));
}
return libraries;
}
private void addManifest(JarWriter writer, Class<?>[] compiledClasses)
throws IOException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
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();
}
protected void addCliClasses(JarWriter writer) throws IOException {
addClass(writer, PackagedSpringApplicationLauncher.class);
addClass(writer, SpringApplicationLauncher.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/")));
}
}
protected final void addClass(JarWriter writer, Class<?> sourceClass)
throws IOException {
addClass(writer, sourceClass.getClassLoader(), sourceClass.getName());
}
protected final void addClass(JarWriter writer, ClassLoader classLoader,
String sourceClass) throws IOException {
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
String name = sourceClass.replace('.', '/') + ".class";
InputStream stream = classLoader.getResourceAsStream(name);
writer.writeEntry(this.layout.getClassesLocation() + name, stream);
}
private void addResource(JarWriter writer, Resource resource, String name)
throws IOException {
InputStream stream = resource.getInputStream();
writer.writeEntry(name, stream);
}
private List<Library> addClasspathEntries(JarWriter writer,
List<MatchedResource> entries) throws IOException {
List<Library> libraries = new ArrayList<>();
for (MatchedResource entry : entries) {
if (entry.isRoot()) {
libraries.add(new Library(entry.getFile(), LibraryScope.COMPILE));
}
else {
writeClasspathEntry(writer, entry);
}
}
return libraries;
}
protected void writeClasspathEntry(JarWriter writer, MatchedResource entry)
throws IOException {
writer.writeEntry(entry.getName(), new FileInputStream(entry.getFile()));
}
protected abstract LibraryScope getLibraryScope(File file);
}
/**
* {@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;
}
// Disable the addition of a static initializer that calls Grape.addResolver
// because all the dependencies are local now
disableGrabResolvers(module.getClasses());
disableGrabResolvers(module.getImports());
}
private void disableGrabResolvers(List<? extends AnnotatedNode> nodes) {
for (AnnotatedNode classNode : nodes) {
List<AnnotationNode> annotations = classNode.getAnnotations();
for (AnnotationNode node : new ArrayList<>(annotations)) {
if (node.getClassNode().getNameWithoutPackage()
.equals("GrabResolver")) {
node.setMember("initClass", new ConstantExpression(false));
}
}
}
}
}
}