Commit 208bf8fc authored by Phillip Webb's avatar Phillip Webb

Polish CLI Jar generation

parent d6486036
......@@ -103,12 +103,6 @@
<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>
......
......@@ -19,13 +19,20 @@ package org.springframework.boot.cli;
import java.io.File;
import org.junit.Test;
import org.springframework.boot.cli.command.jar.JarCommand;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker;
import org.springframework.boot.cli.infrastructure.CommandLineInvoker.Invocation;
import org.springframework.boot.cli.util.JavaExecutable;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Integration test for {@link JarCommand}.
*
* @author Andy Wilkinson
*/
public class JarCommandIT {
......@@ -37,20 +44,18 @@ public class JarCommandIT {
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());
assertThat(invocation.getStandardOutput(), equalTo(""));
assertThat(invocation.getErrorOutput(), containsString("The name of the "
+ "resulting jar and at least one source file must be specified"));
}
@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());
assertThat(invocation.getStandardOutput(), equalTo(""));
assertThat(invocation.getErrorOutput(), containsString("The name of the "
+ "resulting jar and at least one source file must be specified"));
}
@Test
......@@ -62,14 +67,13 @@ public class JarCommandIT {
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();
Process process = new JavaExecutable().processBuilder("-jar",
jar.getAbsolutePath()).start();
invocation = new Invocation(process);
invocation.await();
assertEquals(0, appInvocation.getErrorOutput().length());
assertTrue(appInvocation.getStandardOutput().contains("Hello World!"));
assertTrue(appInvocation.getStandardOutput().contains("/static/test.txt"));
assertThat(invocation.getErrorOutput(), equalTo(""));
assertThat(invocation.getStandardOutput(), containsString("Hello World!"));
assertThat(invocation.getStandardOutput(), containsString("/static/test.txt"));
}
}
......@@ -28,6 +28,7 @@ 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.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
......@@ -36,7 +37,7 @@ import org.springframework.util.AntPathMatcher;
*
* @author Andy Wilkinson
*/
final class ResourceMatcher {
class ResourceMatcher {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
......@@ -49,110 +50,128 @@ final class ResourceMatcher {
this.excludes = excludes;
}
List<MatchedResource> matchResources(List<File> roots) throws IOException {
public List<MatchedResource> find(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));
matchedResources.addAll(findInFolder(root));
}
}
return matchedResources;
}
private List<MatchedResource> matchResources(File root) throws IOException {
List<MatchedResource> resources = new ArrayList<MatchedResource>();
private List<MatchedResource> findInFolder(File folder) throws IOException {
List<MatchedResource> matchedResources = new ArrayList<MatchedResource>();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
new ResourceCollectionResourceLoader(root));
new FolderResourceLoader(folder));
for (String include : this.includes) {
Resource[] candidates = resolver.getResources(include);
for (Resource candidate : candidates) {
for (Resource candidate : resolver.getResources(include)) {
File file = candidate.getFile();
if (file.isFile()) {
MatchedResource matchedResource = new MatchedResource(root, file);
MatchedResource matchedResource = new MatchedResource(folder, file);
if (!isExcluded(matchedResource)) {
resources.add(matchedResource);
matchedResources.add(matchedResource);
}
}
}
}
return resources;
return matchedResources;
}
private boolean isExcluded(MatchedResource matchedResource) {
for (String exclude : this.excludes) {
if (this.pathMatcher.match(exclude, matchedResource.getPath())) {
if (this.pathMatcher.match(exclude, matchedResource.getName())) {
return true;
}
}
return false;
}
private static final class ResourceCollectionResourceLoader extends
DefaultResourceLoader {
private final File root;
/**
* {@link ResourceLoader} to get load resource from a folder.
*/
private static class FolderResourceLoader extends DefaultResourceLoader {
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);
}
private final File rootFolder;
@Override
public URL getResource(String name) {
return findResource(name);
}
});
this.root = root;
public FolderResourceLoader(File root) throws MalformedURLException {
super(new FolderClassLoader(root));
this.rootFolder = root;
}
@Override
protected Resource getResourceByPath(String path) {
return new FileSystemResource(new File(this.root, path));
return new FileSystemResource(new File(this.rootFolder, path));
}
}
static final class MatchedResource {
/**
* {@link ClassLoader} backed by a folder.
*/
private static class FolderClassLoader extends URLClassLoader {
public FolderClassLoader(File rootFolder) throws MalformedURLException {
super(new URL[] { rootFolder.toURI().toURL() });
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return findResources(name);
}
@Override
public URL getResource(String name) {
return findResource(name);
}
}
/**
* A single matched resource.
*/
public static final class MatchedResource {
private final File file;
private final String path;
private final String name;
private final boolean root;
private MatchedResource(File resourceFile) {
this(resourceFile, resourceFile.getName(), true);
private MatchedResource(File file) {
this.name = file.getName();
this.file = file;
this.root = false;
}
private MatchedResource(File root, File resourceFile) {
this(resourceFile, resourceFile.getAbsolutePath().substring(
root.getAbsolutePath().length() + 1), false);
private MatchedResource(File rootFolder, File file) {
this.name = file.getAbsolutePath().substring(
rootFolder.getAbsolutePath().length() + 1);
this.file = file;
this.root = false;
}
private MatchedResource(File resourceFile, String path, boolean root) {
this.file = resourceFile;
this.path = path;
this.name = path;
this.root = root;
}
File getFile() {
return this.file;
public String getName() {
return this.name;
}
String getPath() {
return this.path;
public File getFile() {
return this.file;
}
boolean isRoot() {
public boolean isRoot() {
return this.root;
}
......@@ -160,6 +179,7 @@ final class ResourceMatcher {
public String toString() {
return this.file.getAbsolutePath();
}
}
}
......@@ -16,8 +16,6 @@
package org.springframework.boot.cli.command.shell;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
......@@ -25,8 +23,7 @@ import java.util.List;
import org.springframework.boot.cli.command.Command;
import org.springframework.boot.cli.command.OptionHelp;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.boot.cli.util.JavaExecutable;
/**
* Decorate an existing command to run it by forking the current java process.
......@@ -40,7 +37,7 @@ class ForkProcessCommand extends RunProcessCommand {
private final Command command;
public ForkProcessCommand(Command command) {
super(getJavaCommand());
super(new JavaExecutable().toString());
this.command = command;
}
......@@ -80,24 +77,4 @@ class ForkProcessCommand extends RunProcessCommand {
run(fullArgs);
}
private static String getJavaCommand() {
String javaHome = System.getProperty("java.home");
Assert.state(StringUtils.hasLength(javaHome),
"Unable to find java command to fork process");
try {
return getJavaCommand(javaHome).getCanonicalPath();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static File getJavaCommand(String javaHome) {
File bin = new File(new File(javaHome), "bin");
File command = new File(bin, "java.exe");
command = (command.exists() ? command : new File(bin, "java"));
Assert.state(command.exists(), "Unable to find java in " + javaHome);
return command;
}
}
......@@ -34,7 +34,6 @@ 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;
......@@ -43,8 +42,6 @@ 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;
......@@ -120,6 +117,10 @@ public class GroovyCompiler {
}
}
/**
* Return a mutable list of the {@link ASTTransformation}s to be applied during
* {@link #compile(String...)}.
*/
public List<ASTTransformation> getAstTransformations() {
return this.transformations;
}
......@@ -210,42 +211,6 @@ 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);
......@@ -329,10 +294,4 @@ public class GroovyCompiler {
}
public static interface CompilationCallback {
public void byteCodeGenerated(byte[] byteCode, ClassNode classNode)
throws IOException;
}
}
......@@ -29,6 +29,8 @@ import java.util.jar.Manifest;
*/
public class PackagedSpringApplicationLauncher {
public static final String SOURCE_MANIFEST_ENTRY = "Spring-Application-Source-Classes";
private static final String SPRING_APPLICATION_CLASS = "org.springframework.boot.SpringApplication";
private void run(String[] args) throws Exception {
......@@ -42,7 +44,7 @@ public class PackagedSpringApplicationLauncher {
private Object[] getSources(URLClassLoader classLoader) throws Exception {
URL url = classLoader.findResource("META-INF/MANIFEST.MF");
Manifest manifest = new Manifest(url.openStream());
String attribute = manifest.getMainAttributes().getValue("Application-Classes");
String attribute = manifest.getMainAttributes().getValue(SOURCE_MANIFEST_ENTRY);
return loadClasses(classLoader, attribute.split(","));
}
......
/*
* 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.util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Provides access to the java binary executable, regardless of OS.
*
* @author Phillip Webb
*/
public class JavaExecutable {
private File file;
public JavaExecutable() {
String javaHome = System.getProperty("java.home");
Assert.state(StringUtils.hasLength(javaHome),
"Unable to find java executable due to missing 'java.home'");
this.file = findInJavaHome(javaHome);
}
private File findInJavaHome(String javaHome) {
File bin = new File(new File(javaHome), "bin");
File command = new File(bin, "java.exe");
command = (command.exists() ? command : new File(bin, "java"));
Assert.state(command.exists(), "Unable to find java in " + javaHome);
return command;
}
public ProcessBuilder processBuilder(String... arguments) {
ProcessBuilder processBuilder = new ProcessBuilder(toString());
processBuilder.command().addAll(Arrays.asList(arguments));
return processBuilder;
}
@Override
public String toString() {
try {
return this.file.getCanonicalPath();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
......@@ -29,7 +29,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author awilkinson
* Tests for {@link ResourceMatcher}.
*
* @author Andy Wilkinson
*/
public class ResourceMatcherTests {
......@@ -38,22 +40,21 @@ public class ResourceMatcherTests {
@Test
public void nonExistentRoot() throws IOException {
List<MatchedResource> matchedResources = this.resourceMatcher
.matchResources(Arrays.asList(new File("does-not-exist")));
List<MatchedResource> matchedResources = this.resourceMatcher.find(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(
List<MatchedResource> matchedResources = this.resourceMatcher.find(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());
paths.add(resource.getName());
}
assertEquals(6, paths.size());
......
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