Commit 9f464e9c authored by Phillip Webb's avatar Phillip Webb

Merge branch 'gh-1070'

parents c713c809 f30b962f
...@@ -56,6 +56,8 @@ import org.springframework.boot.cli.jar.PackagedSpringApplicationLauncher; ...@@ -56,6 +56,8 @@ import org.springframework.boot.cli.jar.PackagedSpringApplicationLauncher;
import org.springframework.boot.loader.tools.JarWriter; import org.springframework.boot.loader.tools.JarWriter;
import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.Layout;
import org.springframework.boot.loader.tools.Layouts; 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.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -248,7 +250,8 @@ public class JarCommand extends OptionParsingCommand { ...@@ -248,7 +250,8 @@ public class JarCommand extends OptionParsingCommand {
private void addDependency(JarWriter writer, File dependency) private void addDependency(JarWriter writer, File dependency)
throws FileNotFoundException, IOException { throws FileNotFoundException, IOException {
if (dependency.isFile()) { if (dependency.isFile()) {
writer.writeNestedLibrary("lib/", dependency); writer.writeNestedLibrary("lib/", new Library(dependency,
LibraryScope.COMPILE));
} }
} }
......
...@@ -511,6 +511,11 @@ The following configuration options are available: ...@@ -511,6 +511,11 @@ The following configuration options are available:
|`layout` |`layout`
|The type of archive, corresponding to how the dependencies are laid out inside |The type of archive, corresponding to how the dependencies are laid out inside
(defaults to a guess based on the archive type). (defaults to a guess based on the archive type).
|`requiresUnpack`
|A list of dependencies (in the form ``groupId:artifactId'' that must be unpacked from
fat jars in order to run. Items are still packaged into the fat jar, but they will be
automatically unpacked when it runs.
|=== |===
...@@ -619,7 +624,7 @@ Here is a typical example repackage: ...@@ -619,7 +624,7 @@ Here is a typical example repackage:
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
// Build system specific implementation, callback for each dependency // Build system specific implementation, callback for each dependency
// callback.library(nestedFile, LibraryScope.COMPILE); // callback.library(new Library(nestedFile, LibraryScope.COMPILE));
} }
}); });
---- ----
......
...@@ -1618,6 +1618,50 @@ For Gradle users the steps are similar. Example: ...@@ -1618,6 +1618,50 @@ For Gradle users the steps are similar. Example:
[[howto-extract-specific-libraries-when-an-executable-jar-runs]]
=== Extract specific libraries when an executable jar runs
Most nested libraries in an executable jar do not need to be unpacked in order to run,
however, certain libraries can have problems. For example, JRuby includes its own nested
jar support which assumes that the `jruby-complete.jar` is always directly available as a
file in its own right.
To deal with any problematic libraries, you can flag that specific nested jars should be
automatically unpacked to the ``temp folder'' when the executable jar first runs.
For example, to indicate that JRuby should be flagged for unpack using the Maven Plugin
you would add the following configuration:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<requiresUnpack>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</plugin>
</plugins>
</build>
----
And to do that same with Gradle:
[source,groovy,indent=0,subs="verbatim,attributes"]
----
springBoot {
requiresUnpack = ['org.jruby:jruby-complete']
}
----
[[howto-create-a-nonexecutable-jar]] [[howto-create-a-nonexecutable-jar]]
=== Create a non-executable JAR with exclusions === Create a non-executable JAR with exclusions
Often if you have an executable and a non-executable jar as build products, the executable Often if you have an executable and a non-executable jar as build products, the executable
......
...@@ -107,6 +107,12 @@ public class SpringBootPluginExtension { ...@@ -107,6 +107,12 @@ public class SpringBootPluginExtension {
(layout == null ? null : layout.layout) (layout == null ? null : layout.layout)
} }
/**
* Libraries that must be unpacked from fat jars in order to run. Use Strings in the
* form {@literal groupId:artifactId}.
*/
Set<String> requiresUnpack;
/** /**
* Location of an agent jar to attach to the VM when running the application with runJar task. * Location of an agent jar to attach to the VM when running the application with runJar task.
*/ */
...@@ -121,4 +127,5 @@ public class SpringBootPluginExtension { ...@@ -121,4 +127,5 @@ public class SpringBootPluginExtension {
* If exclude rules should be applied to dependencies based on the spring-dependencies-bom * If exclude rules should be applied to dependencies based on the spring-dependencies-bom
*/ */
boolean applyExcludeRules = true; boolean applyExcludeRules = true;
} }
...@@ -18,11 +18,17 @@ package org.springframework.boot.gradle.repackage; ...@@ -18,11 +18,17 @@ package org.springframework.boot.gradle.repackage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.FileCollection; import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.springframework.boot.gradle.SpringBootPluginExtension;
import org.springframework.boot.loader.tools.Libraries; 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.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.boot.loader.tools.LibraryScope;
...@@ -36,22 +42,24 @@ class ProjectLibraries implements Libraries { ...@@ -36,22 +42,24 @@ class ProjectLibraries implements Libraries {
private final Project project; private final Project project;
private final SpringBootPluginExtension extension;
private String providedConfigurationName = "providedRuntime"; private String providedConfigurationName = "providedRuntime";
private String customConfigurationName = null; private String customConfigurationName = null;
/** /**
* Create a new {@link ProjectLibraries} instance of the specified {@link Project}. * Create a new {@link ProjectLibraries} instance of the specified {@link Project}.
*
* @param project the gradle project * @param project the gradle project
* @param extension the extension
*/ */
public ProjectLibraries(Project project) { public ProjectLibraries(Project project, SpringBootPluginExtension extension) {
this.project = project; this.project = project;
this.extension = extension;
} }
/** /**
* Set the name of the provided configuration. Defaults to 'providedRuntime'. * Set the name of the provided configuration. Defaults to 'providedRuntime'.
*
* @param providedConfigurationName the providedConfigurationName to set * @param providedConfigurationName the providedConfigurationName to set
*/ */
public void setProvidedConfigurationName(String providedConfigurationName) { public void setProvidedConfigurationName(String providedConfigurationName) {
...@@ -64,27 +72,20 @@ class ProjectLibraries implements Libraries { ...@@ -64,27 +72,20 @@ class ProjectLibraries implements Libraries {
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
Set<ResolvedArtifact> custom = getArtifacts(this.customConfigurationName);
FileCollection custom = this.customConfigurationName != null ? this.project
.getConfigurations().findByName(this.customConfigurationName) : null;
if (custom != null) { if (custom != null) {
libraries(LibraryScope.CUSTOM, custom, callback); libraries(LibraryScope.CUSTOM, custom, callback);
} }
else { else {
FileCollection compile = this.project.getConfigurations() Set<ResolvedArtifact> compile = getArtifacts("compile");
.getByName("compile");
FileCollection runtime = this.project.getConfigurations()
.getByName("runtime");
runtime = runtime.minus(compile);
FileCollection provided = this.project.getConfigurations() Set<ResolvedArtifact> runtime = getArtifacts("runtime");
.findByName(this.providedConfigurationName); runtime = minus(runtime, compile);
Set<ResolvedArtifact> provided = getArtifacts(this.providedConfigurationName);
if (provided != null) { if (provided != null) {
compile = compile.minus(provided); compile = minus(compile, provided);
runtime = runtime.minus(provided); runtime = minus(runtime, provided);
} }
libraries(LibraryScope.COMPILE, compile, callback); libraries(LibraryScope.COMPILE, compile, callback);
...@@ -93,12 +94,47 @@ class ProjectLibraries implements Libraries { ...@@ -93,12 +94,47 @@ class ProjectLibraries implements Libraries {
} }
} }
private void libraries(LibraryScope scope, FileCollection files, private Set<ResolvedArtifact> getArtifacts(String configurationName) {
Configuration configuration = (configurationName == null ? null : this.project
.getConfigurations().findByName(configurationName));
return (configuration == null ? null : configuration.getResolvedConfiguration()
.getResolvedArtifacts());
}
private Set<ResolvedArtifact> minus(Set<ResolvedArtifact> source,
Set<ResolvedArtifact> toRemove) {
if (source == null || toRemove == null) {
return source;
}
Set<File> filesToRemove = new HashSet<File>();
for (ResolvedArtifact artifact : toRemove) {
filesToRemove.add(artifact.getFile());
}
Set<ResolvedArtifact> result = new LinkedHashSet<ResolvedArtifact>();
for (ResolvedArtifact artifact : source) {
if (!toRemove.contains(artifact.getFile())) {
result.add(artifact);
}
}
return result;
}
private void libraries(LibraryScope scope, Set<ResolvedArtifact> artifacts,
LibraryCallback callback) throws IOException { LibraryCallback callback) throws IOException {
if (files != null) { if (artifacts != null) {
for (File file: files) { for (ResolvedArtifact artifact : artifacts) {
callback.library(file, scope); callback.library(new Library(artifact.getFile(), scope, isUnpackRequired(artifact)));
} }
} }
} }
private boolean isUnpackRequired(ResolvedArtifact artifact) {
if (this.extension.getRequiresUnpack() != null) {
ModuleVersionIdentifier id = artifact.getModuleVersion().getId();
return this.extension.getRequiresUnpack().contains(
id.getGroup() + ":" + id.getName());
}
return false;
}
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.gradle.repackage; package org.springframework.boot.gradle.repackage;
import java.io.File; import java.io.File;
import java.io.IOException;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.Project; import org.gradle.api.Project;
...@@ -27,6 +28,8 @@ import org.gradle.api.plugins.BasePlugin; ...@@ -27,6 +28,8 @@ import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.gradle.PluginFeatures; import org.springframework.boot.gradle.PluginFeatures;
import org.springframework.boot.gradle.SpringBootPluginExtension; import org.springframework.boot.gradle.SpringBootPluginExtension;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
...@@ -124,11 +127,24 @@ public class RepackagePluginFeatures implements PluginFeatures { ...@@ -124,11 +127,24 @@ public class RepackagePluginFeatures implements PluginFeatures {
+ classifier + "." + StringUtils.getFilenameExtension(outputName); + classifier + "." + StringUtils.getFilenameExtension(outputName);
File outputFile = new File(inputFile.getParentFile(), outputName); File outputFile = new File(inputFile.getParentFile(), outputName);
this.task.getInputs().file(jarTask); this.task.getInputs().file(jarTask);
this.task.getInputs().file(this.task.getDependencies()); addLibraryDependencies(this.task);
this.task.getOutputs().file(outputFile); this.task.getOutputs().file(outputFile);
this.task.setOutputFile(outputFile); this.task.setOutputFile(outputFile);
} }
private void addLibraryDependencies(final RepackageTask task) {
try {
task.getLibraries().doWithLibraries(new LibraryCallback() {
public void library(Library library) throws IOException {
task.getInputs().file(library.getFile());
}
});
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
} }
} }
...@@ -18,8 +18,6 @@ package org.springframework.boot.gradle.repackage; ...@@ -18,8 +18,6 @@ package org.springframework.boot.gradle.repackage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.gradle.api.Action; import org.gradle.api.Action;
...@@ -29,8 +27,6 @@ import org.gradle.api.tasks.TaskAction; ...@@ -29,8 +27,6 @@ import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.gradle.SpringBootPluginExtension; import org.springframework.boot.gradle.SpringBootPluginExtension;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.boot.loader.tools.Repackager; import org.springframework.boot.loader.tools.Repackager;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
...@@ -101,27 +97,11 @@ public class RepackageTask extends DefaultTask { ...@@ -101,27 +97,11 @@ public class RepackageTask extends DefaultTask {
project.getTasks().withType(Jar.class, new RepackageAction(extension, libraries)); project.getTasks().withType(Jar.class, new RepackageAction(extension, libraries));
} }
public File[] getDependencies() { public ProjectLibraries getLibraries() {
ProjectLibraries libraries = getLibraries();
final List<File> files = new ArrayList<File>();
try {
libraries.doWithLibraries(new LibraryCallback() {
@Override
public void library(File file, LibraryScope scope) throws IOException {
files.add(file);
}
});
} catch (IOException ex) {
throw new IllegalStateException("Cannot retrieve dependencies", ex);
}
return files.toArray(new File[files.size()]);
}
private ProjectLibraries getLibraries() {
Project project = getProject(); Project project = getProject();
SpringBootPluginExtension extension = project.getExtensions().getByType( SpringBootPluginExtension extension = project.getExtensions().getByType(
SpringBootPluginExtension.class); SpringBootPluginExtension.class);
ProjectLibraries libraries = new ProjectLibraries(project); ProjectLibraries libraries = new ProjectLibraries(project, extension);
if (extension.getProvidedConfiguration() != null) { if (extension.getProvidedConfiguration() != null) {
libraries.setProvidedConfigurationName(extension.getProvidedConfiguration()); libraries.setProvidedConfigurationName(extension.getProvidedConfiguration());
} }
......
...@@ -17,13 +17,19 @@ ...@@ -17,13 +17,19 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** /**
* Utilities for manipulating files and directories in Spring Boot tooling. * Utilities for manipulating files and directories in Spring Boot tooling.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
*/ */
public class FileUtils { public abstract class FileUtils {
/** /**
* Utility to remove duplicate files from an "output" directory if they already exist * Utility to remove duplicate files from an "output" directory if they already exist
...@@ -50,4 +56,37 @@ public class FileUtils { ...@@ -50,4 +56,37 @@ public class FileUtils {
} }
} }
/**
* Generate a SHA.1 Hash for a given file.
* @param file the file to hash
* @return the hash value as a String
* @throws IOException
*/
public static String sha1Hash(File file) throws IOException {
try {
DigestInputStream inputStream = new DigestInputStream(new FileInputStream(
file), MessageDigest.getInstance("SHA-1"));
try {
byte[] buffer = new byte[4098];
while (inputStream.read(buffer) != -1) {
// Read the entire stream
}
return bytesToHex(inputStream.getMessageDigest().digest());
}
finally {
inputStream.close();
}
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
} }
...@@ -50,7 +50,7 @@ public class JarWriter { ...@@ -50,7 +50,7 @@ public class JarWriter {
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = 32 * 1024;
private final JarOutputStream jarOutput; private final JarOutputStream jarOutput;
...@@ -122,11 +122,16 @@ public class JarWriter { ...@@ -122,11 +122,16 @@ public class JarWriter {
/** /**
* Write a nested library. * Write a nested library.
* @param destination the destination of the library * @param destination the destination of the library
* @param file the library file * @param library the library
* @throws IOException if the write fails * @throws IOException if the write fails
*/ */
public void writeNestedLibrary(String destination, File file) throws IOException { public void writeNestedLibrary(String destination, Library library)
throws IOException {
File file = library.getFile();
JarEntry entry = new JarEntry(destination + file.getName()); JarEntry entry = new JarEntry(destination + file.getName());
if (library.isUnpackRequired()) {
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
}
new CrcAndSize(file).setupStoredEntry(entry); new CrcAndSize(file).setupStoredEntry(entry);
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true)); writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
} }
......
/*
* 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.tools;
import java.io.File;
/**
* Encapsulates information about a single library that may be packed into the archive.
*
* @author Phillip Webb
* @since 1.1.2
* @see Libraries
*/
public class Library {
private final File file;
private final LibraryScope scope;
private final boolean unpackRequired;
/**
* Create a new {@link Library}.
* @param file the source file
* @param scope the scope of the library
*/
public Library(File file, LibraryScope scope) {
this(file, scope, false);
}
/**
* Create a new {@link Library}.
* @param file the source file
* @param scope the scope of the library
* @param unpackRequired if the library needs to be unpacked before it can be used
*/
public Library(File file, LibraryScope scope, boolean unpackRequired) {
this.file = file;
this.scope = scope;
this.unpackRequired = unpackRequired;
}
/**
* @return the library file
*/
public File getFile() {
return this.file;
}
/**
* @return the scope of the library
*/
public LibraryScope getScope() {
return this.scope;
}
/**
* @return if the file cannot be used directly as a nested jar and needs to be
* unpacked.
*/
public boolean isUnpackRequired() {
return this.unpackRequired;
}
}
...@@ -28,10 +28,9 @@ public interface LibraryCallback { ...@@ -28,10 +28,9 @@ public interface LibraryCallback {
/** /**
* Callback to for a single library backed by a {@link File}. * Callback to for a single library backed by a {@link File}.
* @param file the library file * @param library the library
* @param scope the scope of the library
* @throws IOException * @throws IOException
*/ */
void library(File file, LibraryScope scope) throws IOException; void library(Library library) throws IOException;
} }
...@@ -141,12 +141,13 @@ public class Repackager { ...@@ -141,12 +141,13 @@ public class Repackager {
libraries.doWithLibraries(new LibraryCallback() { libraries.doWithLibraries(new LibraryCallback() {
@Override @Override
public void library(File file, LibraryScope scope) throws IOException { public void library(Library library) throws IOException {
File file = library.getFile();
if (isZip(file)) { if (isZip(file)) {
String destination = Repackager.this.layout String destination = Repackager.this.layout
.getLibraryDestination(file.getName(), scope); .getLibraryDestination(file.getName(), library.getScope());
if (destination != null) { if (destination != null) {
writer.writeNestedLibrary(destination, file); writer.writeNestedLibrary(destination, library);
} }
} }
} }
......
...@@ -17,22 +17,32 @@ ...@@ -17,22 +17,32 @@
package org.springframework.boot.loader.tools; package org.springframework.boot.loader.tools;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
* Tests fir {@link FileUtils}. * Tests fir {@link FileUtils}.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
*/ */
public class FileUtilsTests { public class FileUtilsTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File outputDirectory; private File outputDirectory;
private File originDirectory; private File originDirectory;
...@@ -91,4 +101,18 @@ public class FileUtilsTests { ...@@ -91,4 +101,18 @@ public class FileUtilsTests {
assertTrue(file.exists()); assertTrue(file.exists());
} }
@Test
public void hash() throws Exception {
File file = this.temporaryFolder.newFile();
OutputStream outputStream = new FileOutputStream(file);
try {
outputStream.write(new byte[] { 1, 2, 3 });
}
finally {
outputStream.close();
}
assertThat(FileUtils.sha1Hash(file),
equalTo("7037807198c22a7d2b0807371d763779a84fdfcf"));
}
} }
...@@ -19,6 +19,7 @@ package org.springframework.boot.loader.tools; ...@@ -19,6 +19,7 @@ package org.springframework.boot.loader.tools;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.jar.Attributes; import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
...@@ -33,6 +34,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod; ...@@ -33,6 +34,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
...@@ -258,6 +260,7 @@ public class RepackagerTests { ...@@ -258,6 +260,7 @@ public class RepackagerTests {
TestJarFile libJar = new TestJarFile(this.temporaryFolder); TestJarFile libJar = new TestJarFile(this.temporaryFolder);
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
final File libJarFile = libJar.getFile(); final File libJarFile = libJar.getFile();
final File libJarFileToUnpack = libJar.getFile();
final File libNonJarFile = this.temporaryFolder.newFile(); final File libNonJarFile = this.temporaryFolder.newFile();
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile); FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
...@@ -266,12 +269,18 @@ public class RepackagerTests { ...@@ -266,12 +269,18 @@ public class RepackagerTests {
repackager.repackage(new Libraries() { repackager.repackage(new Libraries() {
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(libJarFile, LibraryScope.COMPILE); callback.library(new Library(libJarFile, LibraryScope.COMPILE));
callback.library(libNonJarFile, LibraryScope.COMPILE); callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE,
true));
callback.library(new Library(libNonJarFile, LibraryScope.COMPILE));
} }
}); });
assertThat(hasEntry(file, "lib/" + libJarFile.getName()), equalTo(true)); assertThat(hasEntry(file, "lib/" + libJarFile.getName()), equalTo(true));
assertThat(hasEntry(file, "lib/" + libJarFileToUnpack.getName()), equalTo(true));
assertThat(hasEntry(file, "lib/" + libNonJarFile.getName()), equalTo(false)); assertThat(hasEntry(file, "lib/" + libNonJarFile.getName()), equalTo(false));
JarEntry entry = getEntry(file, "lib/" + libJarFileToUnpack.getName());
assertThat(entry.getComment(), startsWith("UNPACK:"));
assertThat(entry.getComment().length(), equalTo(47));
} }
@Test @Test
...@@ -290,7 +299,7 @@ public class RepackagerTests { ...@@ -290,7 +299,7 @@ public class RepackagerTests {
repackager.repackage(new Libraries() { repackager.repackage(new Libraries() {
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(libJarFile, scope); callback.library(new Library(libJarFile, scope));
} }
}); });
assertThat(hasEntry(file, "test/" + libJarFile.getName()), equalTo(true)); assertThat(hasEntry(file, "test/" + libJarFile.getName()), equalTo(true));
...@@ -331,7 +340,7 @@ public class RepackagerTests { ...@@ -331,7 +340,7 @@ public class RepackagerTests {
repackager.repackage(new Libraries() { repackager.repackage(new Libraries() {
@Override @Override
public void doWithLibraries(LibraryCallback callback) throws IOException { public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(nestedFile, LibraryScope.COMPILE); callback.library(new Library(nestedFile, LibraryScope.COMPILE));
} }
}); });
...@@ -345,7 +354,6 @@ public class RepackagerTests { ...@@ -345,7 +354,6 @@ public class RepackagerTests {
finally { finally {
jarFile.close(); jarFile.close();
} }
} }
private boolean hasLauncherClasses(File file) throws IOException { private boolean hasLauncherClasses(File file) throws IOException {
...@@ -354,9 +362,13 @@ public class RepackagerTests { ...@@ -354,9 +362,13 @@ public class RepackagerTests {
} }
private boolean hasEntry(File file, String name) throws IOException { private boolean hasEntry(File file, String name) throws IOException {
return getEntry(file, name) != null;
}
private JarEntry getEntry(File file, String name) throws IOException {
JarFile jarFile = new JarFile(file); JarFile jarFile = new JarFile(file);
try { try {
return jarFile.getEntry(name) != null; return jarFile.getJarEntry(name);
} }
finally { finally {
jarFile.close(); jarFile.close();
......
...@@ -17,7 +17,10 @@ ...@@ -17,7 +17,10 @@
package org.springframework.boot.loader.archive; package org.springframework.boot.loader.archive;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -27,6 +30,7 @@ import java.util.List; ...@@ -27,6 +30,7 @@ import java.util.List;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.jar.JarEntryData; import org.springframework.boot.loader.jar.JarEntryData;
import org.springframework.boot.loader.jar.JarEntryFilter; import org.springframework.boot.loader.jar.JarEntryFilter;
import org.springframework.boot.loader.jar.JarFile; import org.springframework.boot.loader.jar.JarFile;
...@@ -39,12 +43,23 @@ import org.springframework.boot.loader.util.AsciiBytes; ...@@ -39,12 +43,23 @@ import org.springframework.boot.loader.util.AsciiBytes;
*/ */
public class JarFileArchive extends Archive { public class JarFileArchive extends Archive {
private static final AsciiBytes UNPACK_MARKER = new AsciiBytes("UNPACK:");
private static final int BUFFER_SIZE = 32 * 1024;
private final JarFile jarFile; private final JarFile jarFile;
private final List<Entry> entries; private final List<Entry> entries;
private URL url;
public JarFileArchive(File file) throws IOException { public JarFileArchive(File file) throws IOException {
this(file, null);
}
public JarFileArchive(File file, URL url) throws IOException {
this(new JarFile(file)); this(new JarFile(file));
this.url = url;
} }
public JarFileArchive(JarFile jarFile) { public JarFileArchive(JarFile jarFile) {
...@@ -58,6 +73,9 @@ public class JarFileArchive extends Archive { ...@@ -58,6 +73,9 @@ public class JarFileArchive extends Archive {
@Override @Override
public URL getUrl() throws MalformedURLException { public URL getUrl() throws MalformedURLException {
if (this.url != null) {
return this.url;
}
return this.jarFile.getUrl(); return this.jarFile.getUrl();
} }
...@@ -84,10 +102,54 @@ public class JarFileArchive extends Archive { ...@@ -84,10 +102,54 @@ public class JarFileArchive extends Archive {
protected Archive getNestedArchive(Entry entry) throws IOException { protected Archive getNestedArchive(Entry entry) throws IOException {
JarEntryData data = ((JarFileEntry) entry).getJarEntryData(); JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
if (data.getComment().startsWith(UNPACK_MARKER)) {
return getUnpackedNestedArchive(data);
}
JarFile jarFile = this.jarFile.getNestedJarFile(data); JarFile jarFile = this.jarFile.getNestedJarFile(data);
return new JarFileArchive(jarFile); return new JarFileArchive(jarFile);
} }
private Archive getUnpackedNestedArchive(JarEntryData data) throws IOException {
AsciiBytes hash = data.getComment().substring(UNPACK_MARKER.length());
String name = data.getName().toString();
if (name.lastIndexOf("/") != -1) {
name = name.substring(name.lastIndexOf("/") + 1);
}
File file = new File(getTempUnpackFolder(), hash.toString() + "-" + name);
if (!file.exists() || file.length() != data.getSize()) {
unpack(data, file);
}
return new JarFileArchive(file, file.toURI().toURL());
}
private File getTempUnpackFolder() {
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
File unpackFolder = new File(tempFolder, "spring-boot-libs");
unpackFolder.mkdirs();
return unpackFolder;
}
private void unpack(JarEntryData data, File file) throws IOException {
InputStream inputStream = data.getData().getInputStream(ResourceAccess.ONCE);
try {
OutputStream outputStream = new FileOutputStream(file);
try {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
finally {
outputStream.close();
}
}
finally {
inputStream.close();
}
}
@Override @Override
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() { JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {
......
...@@ -83,7 +83,7 @@ public class Handler extends URLStreamHandler { ...@@ -83,7 +83,7 @@ public class Handler extends URLStreamHandler {
return new JarURLConnection(url, this.jarFile); return new JarURLConnection(url, this.jarFile);
} }
try { try {
return new JarURLConnection(url, getJarFileFromUrl(url)); return new JarURLConnection(url, getRootJarFileFromUrl(url));
} }
catch (Exception ex) { catch (Exception ex) {
return openFallbackConnection(url, ex); return openFallbackConnection(url, ex);
...@@ -96,10 +96,11 @@ public class Handler extends URLStreamHandler { ...@@ -96,10 +96,11 @@ public class Handler extends URLStreamHandler {
return openConnection(getFallbackHandler(), url); return openConnection(getFallbackHandler(), url);
} }
catch (Exception ex) { catch (Exception ex) {
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
if (reason instanceof IOException) { if (reason instanceof IOException) {
this.logger.log(Level.FINEST, "Unable to open fallback handler", ex);
throw (IOException) reason; throw (IOException) reason;
} }
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
if (reason instanceof RuntimeException) { if (reason instanceof RuntimeException) {
throw (RuntimeException) reason; throw (RuntimeException) reason;
} }
...@@ -111,7 +112,6 @@ public class Handler extends URLStreamHandler { ...@@ -111,7 +112,6 @@ public class Handler extends URLStreamHandler {
if (this.fallbackHandler != null) { if (this.fallbackHandler != null) {
return this.fallbackHandler; return this.fallbackHandler;
} }
for (String handlerClassName : FALLBACK_HANDLERS) { for (String handlerClassName : FALLBACK_HANDLERS) {
try { try {
Class<?> handlerClass = Class.forName(handlerClassName); Class<?> handlerClass = Class.forName(handlerClassName);
...@@ -135,24 +135,14 @@ public class Handler extends URLStreamHandler { ...@@ -135,24 +135,14 @@ public class Handler extends URLStreamHandler {
return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url); return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url);
} }
public JarFile getJarFileFromUrl(URL url) throws IOException { public JarFile getRootJarFileFromUrl(URL url) throws IOException {
String spec = url.getFile(); String spec = url.getFile();
int separatorIndex = spec.indexOf(SEPARATOR); int separatorIndex = spec.indexOf(SEPARATOR);
if (separatorIndex == -1) { if (separatorIndex == -1) {
throw new MalformedURLException("Jar URL does not contain !/ separator"); throw new MalformedURLException("Jar URL does not contain !/ separator");
} }
String name = spec.substring(0, separatorIndex);
JarFile jar = null; return getRootJarFile(name);
while (separatorIndex != -1) {
String name = spec.substring(0, separatorIndex);
jar = (jar == null ? getRootJarFile(name) : getNestedJarFile(jar, name));
spec = spec.substring(separatorIndex + SEPARATOR.length());
separatorIndex = spec.indexOf(SEPARATOR);
}
return jar;
} }
private JarFile getRootJarFile(String name) throws IOException { private JarFile getRootJarFile(String name) throws IOException {
...@@ -175,15 +165,6 @@ public class Handler extends URLStreamHandler { ...@@ -175,15 +165,6 @@ public class Handler extends URLStreamHandler {
} }
} }
private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(name);
if (jarEntry == null) {
throw new IOException("Unable to find nested jar '" + name + "' from '"
+ jarFile + "'");
}
return jarFile.getNestedJarFile(jarEntry);
}
/** /**
* Add the given {@link JarFile} to the root file cache. * Add the given {@link JarFile} to the root file cache.
* @param sourceFile the source file to add * @param sourceFile the source file to add
......
...@@ -93,7 +93,13 @@ public final class JarEntryData { ...@@ -93,7 +93,13 @@ public final class JarEntryData {
return inputStream; return inputStream;
} }
RandomAccessData getData() throws IOException { /**
* @return the underlying {@link RandomAccessData} for this entry. Generally this
* method should not be called directly and instead data should be accessed via
* {@link JarFile#getInputStream(ZipEntry)}.
* @throws IOException
*/
public RandomAccessData getData() throws IOException {
if (this.data == null) { if (this.data == null) {
// aspectjrt-1.7.4.jar has a different ext bytes length in the // aspectjrt-1.7.4.jar has a different ext bytes length in the
// local directory to the central directory. We need to re-read // local directory to the central directory. We need to re-read
......
...@@ -61,8 +61,6 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -61,8 +61,6 @@ class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>(); private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private final String jarFileUrlSpec;
private final JarFile jarFile; private final JarFile jarFile;
private JarEntryData jarEntryData; private JarEntryData jarEntryData;
...@@ -71,19 +69,26 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -71,19 +69,26 @@ class JarURLConnection extends java.net.JarURLConnection {
private JarEntryName jarEntryName; private JarEntryName jarEntryName;
protected JarURLConnection(URL url, JarFile jarFile) throws MalformedURLException { protected JarURLConnection(URL url, JarFile jarFile) throws IOException {
// What we pass to super is ultimately ignored // What we pass to super is ultimately ignored
super(EMPTY_JAR_URL); super(EMPTY_JAR_URL);
this.url = url; this.url = url;
String spec = url.getFile().substring(jarFile.getUrl().getFile().length());
int separator;
while ((separator = spec.indexOf(SEPARATOR)) > 0) {
jarFile = getNestedJarFile(jarFile, spec.substring(0, separator));
spec = spec.substring(separator + SEPARATOR.length());
}
this.jarFile = jarFile; this.jarFile = jarFile;
String spec = url.getFile(); this.jarEntryName = getJarEntryName(spec);
int separator = spec.lastIndexOf(SEPARATOR); }
if (separator == -1) {
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:" private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
+ spec); JarEntry jarEntry = jarFile.getJarEntry(name);
if (jarEntry == null) {
throwFileNotFound(jarEntry, jarFile);
} }
this.jarFileUrlSpec = spec.substring(0, separator); return jarFile.getNestedJarFile(jarEntry);
this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
} }
private JarEntryName getJarEntryName(String spec) { private JarEntryName getJarEntryName(String spec) {
...@@ -99,16 +104,20 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -99,16 +104,20 @@ class JarURLConnection extends java.net.JarURLConnection {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
.asAsciiBytes()); .asAsciiBytes());
if (this.jarEntryData == null) { if (this.jarEntryData == null) {
if (Boolean.TRUE.equals(useFastExceptions.get())) { throwFileNotFound(this.jarEntryName, this.jarFile);
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName());
} }
} }
this.connected = true; this.connected = true;
} }
private void throwFileNotFound(Object entry, JarFile jarFile) throws FileNotFoundException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + entry + " not found in "
+ jarFile.getName());
}
@Override @Override
public Manifest getManifest() throws IOException { public Manifest getManifest() throws IOException {
try { try {
...@@ -135,10 +144,14 @@ class JarURLConnection extends java.net.JarURLConnection { ...@@ -135,10 +144,14 @@ class JarURLConnection extends java.net.JarURLConnection {
private URL buildJarFileUrl() { private URL buildJarFileUrl() {
try { try {
if (this.jarFileUrlSpec.indexOf(SEPARATOR) == -1) { String spec = this.jarFile.getUrl().getFile();
return new URL(this.jarFileUrlSpec); if (spec.endsWith(SEPARATOR)) {
spec = spec.substring(0, spec.length() - SEPARATOR.length());
}
if (spec.indexOf(SEPARATOR) == -1) {
return new URL(spec);
} }
return new URL("jar:" + this.jarFileUrlSpec); return new URL("jar:" + spec);
} }
catch (MalformedURLException ex) { catch (MalformedURLException ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
......
...@@ -16,21 +16,32 @@ ...@@ -16,21 +16,32 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import java.io.File;
import java.net.URL; import java.net.URL;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.jar.JarFile;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
* Tests for {@link LaunchedURLClassLoader}. * Tests for {@link LaunchedURLClassLoader}.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
*/ */
@SuppressWarnings("resource")
public class LaunchedURLClassLoaderTests { public class LaunchedURLClassLoaderTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test @Test
public void resolveResourceFromWindowsFilesystem() throws Exception { public void resolveResourceFromWindowsFilesystem() throws Exception {
// This path is invalid - it should return null even on Windows. // This path is invalid - it should return null even on Windows.
...@@ -76,4 +87,17 @@ public class LaunchedURLClassLoaderTests { ...@@ -76,4 +87,17 @@ public class LaunchedURLClassLoaderTests {
assertTrue(loader.getResources("").hasMoreElements()); assertTrue(loader.getResources("").hasMoreElements());
} }
@Test
public void resolveFromNested() throws Exception {
File file = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(file);
JarFile jarFile = new JarFile(file);
URL url = jarFile.getUrl();
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url },
null);
URL resource = loader.getResource("nested.jar!/3.dat");
assertThat(resource.toString(), equalTo(url + "nested.jar!/3.dat"));
assertThat(resource.openConnection().getInputStream().read(), equalTo(3));
}
} }
...@@ -35,6 +35,10 @@ import java.util.zip.ZipEntry; ...@@ -35,6 +35,10 @@ import java.util.zip.ZipEntry;
public abstract class TestJarCreator { public abstract class TestJarCreator {
public static void createTestJar(File file) throws Exception { public static void createTestJar(File file) throws Exception {
createTestJar(file, false);
}
public static void createTestJar(File file, boolean unpackNested) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream(file); FileOutputStream fileOutputStream = new FileOutputStream(file);
JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream); JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream);
try { try {
...@@ -50,6 +54,9 @@ public abstract class TestJarCreator { ...@@ -50,6 +54,9 @@ public abstract class TestJarCreator {
byte[] nestedJarData = getNestedJarData(); byte[] nestedJarData = getNestedJarData();
nestedEntry.setSize(nestedJarData.length); nestedEntry.setSize(nestedJarData.length);
nestedEntry.setCompressedSize(nestedJarData.length); nestedEntry.setCompressedSize(nestedJarData.length);
if (unpackNested) {
nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000");
}
CRC32 crc32 = new CRC32(); CRC32 crc32 = new CRC32();
crc32.update(nestedJarData); crc32.update(nestedJarData);
nestedEntry.setCrc(crc32.getValue()); nestedEntry.setCrc(crc32.getValue());
......
...@@ -29,7 +29,9 @@ import org.springframework.boot.loader.TestJarCreator; ...@@ -29,7 +29,9 @@ import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive.Entry; import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.util.AsciiBytes; import org.springframework.boot.loader.util.AsciiBytes;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
/** /**
...@@ -48,8 +50,12 @@ public class JarFileArchiveTests { ...@@ -48,8 +50,12 @@ public class JarFileArchiveTests {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
setup(false);
}
private void setup(boolean unpackNested) throws Exception {
this.rootJarFile = this.temporaryFolder.newFile(); this.rootJarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.rootJarFile); TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
this.archive = new JarFileArchive(this.rootJarFile); this.archive = new JarFileArchive(this.rootJarFile);
} }
...@@ -80,6 +86,15 @@ public class JarFileArchiveTests { ...@@ -80,6 +86,15 @@ public class JarFileArchiveTests {
equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/")); equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/"));
} }
@Test
public void getNestedUnpackedArchive() throws Exception {
setup(true);
Entry entry = getEntriesMap(this.archive).get("nested.jar");
Archive nested = this.archive.getNestedArchive(entry);
assertThat(nested.getUrl().toString(), startsWith("file:"));
assertThat(nested.getUrl().toString(), endsWith(".jar"));
}
@Test @Test
public void getFilteredArchive() throws Exception { public void getFilteredArchive() throws Exception {
Archive filteredArchive = this.archive Archive filteredArchive = this.archive
......
...@@ -408,4 +408,16 @@ public class JarFileTests { ...@@ -408,4 +408,16 @@ public class JarFileTests {
getEntries(); getEntries();
getNestedJarFile(); getNestedJarFile();
} }
@Test
public void cannotLoadMissingJar() throws Exception {
// relates to gh-1070
JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile
.getEntry("nested.jar"));
URL nestedUrl = nestedJarFile.getUrl();
URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat");
this.thrown.expect(FileNotFoundException.class);
url.openConnection().getInputStream();
}
} }
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-with-unpack</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<requiresUnpack>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</project>
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}
/*
* 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.
*/
import java.io.*;
import org.springframework.boot.maven.*;
File f = new File( basedir, "target/jar-with-unpack-0.0.1.BUILD-SNAPSHOT.jar");
new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) {
@Override
protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception {
super.verifyZipEntries(verifier)
verifier.hasUnpackEntry("lib/spring-core-4.0.5.RELEASE.jar")
verifier.hasNonUnpackEntry("lib/spring-context-4.0.5.RELEASE.jar")
}
}.verify();
...@@ -17,13 +17,16 @@ ...@@ -17,13 +17,16 @@
package org.springframework.boot.maven; package org.springframework.boot.maven;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.springframework.boot.loader.tools.Libraries; 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.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.boot.loader.tools.LibraryScope;
...@@ -46,8 +49,11 @@ public class ArtifactsLibraries implements Libraries { ...@@ -46,8 +49,11 @@ public class ArtifactsLibraries implements Libraries {
private final Set<Artifact> artifacts; private final Set<Artifact> artifacts;
public ArtifactsLibraries(Set<Artifact> artifacts) { private final Collection<Dependency> unpacks;
public ArtifactsLibraries(Set<Artifact> artifacts, Collection<Dependency> unpacks) {
this.artifacts = artifacts; this.artifacts = artifacts;
this.unpacks = unpacks;
} }
@Override @Override
...@@ -55,8 +61,21 @@ public class ArtifactsLibraries implements Libraries { ...@@ -55,8 +61,21 @@ public class ArtifactsLibraries implements Libraries {
for (Artifact artifact : this.artifacts) { for (Artifact artifact : this.artifacts) {
LibraryScope scope = SCOPES.get(artifact.getScope()); LibraryScope scope = SCOPES.get(artifact.getScope());
if (scope != null && artifact.getFile() != null) { if (scope != null && artifact.getFile() != null) {
callback.library(artifact.getFile(), scope); callback.library(new Library(artifact.getFile(), scope,
isUnpackRequired(artifact)));
}
}
}
private boolean isUnpackRequired(Artifact artifact) {
if (this.unpacks != null) {
for (Dependency unpack : this.unpacks) {
if (artifact.getGroupId().equals(unpack.getGroupId())
&& artifact.getArtifactId().equals(unpack.getArtifactId())) {
return true;
}
} }
} }
return false;
} }
} }
...@@ -18,11 +18,13 @@ package org.springframework.boot.maven; ...@@ -18,11 +18,13 @@ package org.springframework.boot.maven;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Component;
...@@ -108,6 +110,13 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { ...@@ -108,6 +110,13 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter @Parameter
private LayoutType layout; private LayoutType layout;
/**
* A list of the libraries that must be unpacked from fat jars in order to run.
* @since 1.1
*/
@Parameter
private List<Dependency> requiresUnpack;
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException { public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) { if (this.project.getPackaging().equals("pom")) {
...@@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { ...@@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters()); getFilters());
Libraries libraries = new ArtifactsLibraries(artifacts); Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack);
try { try {
repackager.repackage(target, libraries); repackager.repackage(target, libraries);
} }
......
...@@ -21,13 +21,19 @@ import java.util.Collections; ...@@ -21,13 +21,19 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback; import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.boot.loader.tools.LibraryScope;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
...@@ -36,7 +42,7 @@ import static org.mockito.Mockito.verify; ...@@ -36,7 +42,7 @@ import static org.mockito.Mockito.verify;
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class ArtifactsLibrariesTest { public class ArtifactsLibrariesTests {
@Mock @Mock
private Artifact artifact; private Artifact artifact;
...@@ -50,11 +56,14 @@ public class ArtifactsLibrariesTest { ...@@ -50,11 +56,14 @@ public class ArtifactsLibrariesTest {
@Mock @Mock
private LibraryCallback callback; private LibraryCallback callback;
@Captor
private ArgumentCaptor<Library> libraryCaptor;
@Before @Before
public void setup() { public void setup() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
this.artifacts = Collections.singleton(this.artifact); this.artifacts = Collections.singleton(this.artifact);
this.libs = new ArtifactsLibraries(this.artifacts); this.libs = new ArtifactsLibraries(this.artifacts, null);
given(this.artifact.getFile()).willReturn(this.file); given(this.artifact.getFile()).willReturn(this.file);
} }
...@@ -63,6 +72,25 @@ public class ArtifactsLibrariesTest { ...@@ -63,6 +72,25 @@ public class ArtifactsLibrariesTest {
given(this.artifact.getType()).willReturn("jar"); given(this.artifact.getType()).willReturn("jar");
given(this.artifact.getScope()).willReturn("compile"); given(this.artifact.getScope()).willReturn("compile");
this.libs.doWithLibraries(this.callback); this.libs.doWithLibraries(this.callback);
verify(this.callback).library(this.file, LibraryScope.COMPILE); verify(this.callback).library(this.libraryCaptor.capture());
Library library = this.libraryCaptor.getValue();
assertThat(library.getFile(), equalTo(this.file));
assertThat(library.getScope(), equalTo(LibraryScope.COMPILE));
assertThat(library.isUnpackRequired(), equalTo(false));
}
@Test
public void callbackWithUnpack() throws Exception {
given(this.artifact.getGroupId()).willReturn("gid");
given(this.artifact.getArtifactId()).willReturn("aid");
given(this.artifact.getType()).willReturn("jar");
given(this.artifact.getScope()).willReturn("compile");
Dependency unpack = new Dependency();
unpack.setGroupId("gid");
unpack.setArtifactId("aid");
this.libs = new ArtifactsLibraries(this.artifacts, Collections.singleton(unpack));
this.libs.doWithLibraries(this.callback);
verify(this.callback).library(this.libraryCaptor.capture());
assertThat(this.libraryCaptor.getValue().isUnpackRequired(), equalTo(true));
} }
} }
...@@ -87,6 +87,15 @@ public class Verify { ...@@ -87,6 +87,15 @@ public class Verify {
} }
} }
public boolean hasNonUnpackEntry(String entry) {
return !hasUnpackEntry(entry);
}
public boolean hasUnpackEntry(String entry) {
String comment = this.content.get(entry).getComment();
return comment != null && comment.startsWith("UNPACK:");
}
public boolean hasEntry(String entry) { public boolean hasEntry(String entry) {
return this.content.containsKey(entry); return this.content.containsKey(entry);
} }
......
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