Merge branch 'gh-1070'

pull/1165/head
Phillip Webb 11 years ago
commit 9f464e9c49

@ -56,6 +56,8 @@ 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;
@ -248,7 +250,8 @@ public class JarCommand extends OptionParsingCommand {
private void addDependency(JarWriter writer, File dependency)
throws FileNotFoundException, IOException {
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:
|`layout`
|The type of archive, corresponding to how the dependencies are laid out inside
(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:
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
// 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:
[[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]]
=== Create a non-executable JAR with exclusions
Often if you have an executable and a non-executable jar as build products, the executable

@ -107,6 +107,12 @@ public class SpringBootPluginExtension {
(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.
*/
@ -121,4 +127,5 @@ public class SpringBootPluginExtension {
* If exclude rules should be applied to dependencies based on the spring-dependencies-bom
*/
boolean applyExcludeRules = true;
}

@ -18,11 +18,17 @@ package org.springframework.boot.gradle.repackage;
import java.io.File;
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.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.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope;
@ -36,22 +42,24 @@ class ProjectLibraries implements Libraries {
private final Project project;
private final SpringBootPluginExtension extension;
private String providedConfigurationName = "providedRuntime";
private String customConfigurationName = null;
/**
* Create a new {@link ProjectLibraries} instance of the specified {@link Project}.
*
* @param project the gradle project
* @param extension the extension
*/
public ProjectLibraries(Project project) {
public ProjectLibraries(Project project, SpringBootPluginExtension extension) {
this.project = project;
this.extension = extension;
}
/**
* Set the name of the provided configuration. Defaults to 'providedRuntime'.
*
* @param providedConfigurationName the providedConfigurationName to set
*/
public void setProvidedConfigurationName(String providedConfigurationName) {
@ -64,27 +72,20 @@ class ProjectLibraries implements Libraries {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
FileCollection custom = this.customConfigurationName != null ? this.project
.getConfigurations().findByName(this.customConfigurationName) : null;
Set<ResolvedArtifact> custom = getArtifacts(this.customConfigurationName);
if (custom != null) {
libraries(LibraryScope.CUSTOM, custom, callback);
}
else {
FileCollection compile = this.project.getConfigurations()
.getByName("compile");
FileCollection runtime = this.project.getConfigurations()
.getByName("runtime");
runtime = runtime.minus(compile);
Set<ResolvedArtifact> compile = getArtifacts("compile");
FileCollection provided = this.project.getConfigurations()
.findByName(this.providedConfigurationName);
Set<ResolvedArtifact> runtime = getArtifacts("runtime");
runtime = minus(runtime, compile);
Set<ResolvedArtifact> provided = getArtifacts(this.providedConfigurationName);
if (provided != null) {
compile = compile.minus(provided);
runtime = runtime.minus(provided);
compile = minus(compile, provided);
runtime = minus(runtime, provided);
}
libraries(LibraryScope.COMPILE, compile, callback);
@ -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 {
if (files != null) {
for (File file: files) {
callback.library(file, scope);
if (artifacts != null) {
for (ResolvedArtifact artifact : artifacts) {
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 @@
package org.springframework.boot.gradle.repackage;
import java.io.File;
import java.io.IOException;
import org.gradle.api.Action;
import org.gradle.api.Project;
@ -27,6 +28,8 @@ import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.tasks.bundling.Jar;
import org.springframework.boot.gradle.PluginFeatures;
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;
/**
@ -124,11 +127,24 @@ public class RepackagePluginFeatures implements PluginFeatures {
+ classifier + "." + StringUtils.getFilenameExtension(outputName);
File outputFile = new File(inputFile.getParentFile(), outputName);
this.task.getInputs().file(jarTask);
this.task.getInputs().file(this.task.getDependencies());
addLibraryDependencies(this.task);
this.task.getOutputs().file(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;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.gradle.api.Action;
@ -29,8 +27,6 @@ import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.bundling.Jar;
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.util.FileCopyUtils;
@ -101,27 +97,11 @@ public class RepackageTask extends DefaultTask {
project.getTasks().withType(Jar.class, new RepackageAction(extension, libraries));
}
public File[] getDependencies() {
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() {
public ProjectLibraries getLibraries() {
Project project = getProject();
SpringBootPluginExtension extension = project.getExtensions().getByType(
SpringBootPluginExtension.class);
ProjectLibraries libraries = new ProjectLibraries(project);
ProjectLibraries libraries = new ProjectLibraries(project, extension);
if (extension.getProvidedConfiguration() != null) {
libraries.setProvidedConfigurationName(extension.getProvidedConfiguration());
}

@ -17,13 +17,19 @@
package org.springframework.boot.loader.tools;
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.
*
* @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
@ -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 {
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;
@ -122,11 +122,16 @@ public class JarWriter {
/**
* Write a nested library.
* @param destination the destination of the library
* @param file the library file
* @param library the library
* @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());
if (library.isUnpackRequired()) {
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
}
new CrcAndSize(file).setupStoredEntry(entry);
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
}

@ -0,0 +1,79 @@
/*
* 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 {
/**
* Callback to for a single library backed by a {@link File}.
* @param file the library file
* @param scope the scope of the library
* @param library the library
* @throws IOException
*/
void library(File file, LibraryScope scope) throws IOException;
void library(Library library) throws IOException;
}

@ -141,12 +141,13 @@ public class Repackager {
libraries.doWithLibraries(new LibraryCallback() {
@Override
public void library(File file, LibraryScope scope) throws IOException {
public void library(Library library) throws IOException {
File file = library.getFile();
if (isZip(file)) {
String destination = Repackager.this.layout
.getLibraryDestination(file.getName(), scope);
.getLibraryDestination(file.getName(), library.getScope());
if (destination != null) {
writer.writeNestedLibrary(destination, file);
writer.writeNestedLibrary(destination, library);
}
}
}

@ -17,22 +17,32 @@
package org.springframework.boot.loader.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileSystemUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Tests fir {@link FileUtils}.
*
* @author Dave Syer
* @author Phillip Webb
*/
public class FileUtilsTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File outputDirectory;
private File originDirectory;
@ -91,4 +101,18 @@ public class FileUtilsTests {
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;
import java.io.File;
import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
@ -33,6 +34,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyString;
@ -258,6 +260,7 @@ public class RepackagerTests {
TestJarFile libJar = new TestJarFile(this.temporaryFolder);
libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class);
final File libJarFile = libJar.getFile();
final File libJarFileToUnpack = libJar.getFile();
final File libNonJarFile = this.temporaryFolder.newFile();
FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile);
this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class);
@ -266,12 +269,18 @@ public class RepackagerTests {
repackager.repackage(new Libraries() {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(libJarFile, LibraryScope.COMPILE);
callback.library(libNonJarFile, LibraryScope.COMPILE);
callback.library(new Library(libJarFile, 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/" + libJarFileToUnpack.getName()), equalTo(true));
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
@ -290,7 +299,7 @@ public class RepackagerTests {
repackager.repackage(new Libraries() {
@Override
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));
@ -331,7 +340,7 @@ public class RepackagerTests {
repackager.repackage(new Libraries() {
@Override
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 {
finally {
jarFile.close();
}
}
private boolean hasLauncherClasses(File file) throws IOException {
@ -354,9 +362,13 @@ public class RepackagerTests {
}
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);
try {
return jarFile.getEntry(name) != null;
return jarFile.getJarEntry(name);
}
finally {
jarFile.close();

@ -17,7 +17,10 @@
package org.springframework.boot.loader.archive;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
@ -27,6 +30,7 @@ import java.util.List;
import java.util.jar.JarEntry;
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.JarEntryFilter;
import org.springframework.boot.loader.jar.JarFile;
@ -39,12 +43,23 @@ import org.springframework.boot.loader.util.AsciiBytes;
*/
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 List<Entry> entries;
private URL url;
public JarFileArchive(File file) throws IOException {
this(file, null);
}
public JarFileArchive(File file, URL url) throws IOException {
this(new JarFile(file));
this.url = url;
}
public JarFileArchive(JarFile jarFile) {
@ -58,6 +73,9 @@ public class JarFileArchive extends Archive {
@Override
public URL getUrl() throws MalformedURLException {
if (this.url != null) {
return this.url;
}
return this.jarFile.getUrl();
}
@ -84,10 +102,54 @@ public class JarFileArchive extends Archive {
protected Archive getNestedArchive(Entry entry) throws IOException {
JarEntryData data = ((JarFileEntry) entry).getJarEntryData();
if (data.getComment().startsWith(UNPACK_MARKER)) {
return getUnpackedNestedArchive(data);
}
JarFile jarFile = this.jarFile.getNestedJarFile(data);
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
public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException {
JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() {

@ -83,7 +83,7 @@ public class Handler extends URLStreamHandler {
return new JarURLConnection(url, this.jarFile);
}
try {
return new JarURLConnection(url, getJarFileFromUrl(url));
return new JarURLConnection(url, getRootJarFileFromUrl(url));
}
catch (Exception ex) {
return openFallbackConnection(url, ex);
@ -96,10 +96,11 @@ public class Handler extends URLStreamHandler {
return openConnection(getFallbackHandler(), url);
}
catch (Exception ex) {
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
if (reason instanceof IOException) {
this.logger.log(Level.FINEST, "Unable to open fallback handler", ex);
throw (IOException) reason;
}
this.logger.log(Level.WARNING, "Unable to open fallback handler", ex);
if (reason instanceof RuntimeException) {
throw (RuntimeException) reason;
}
@ -111,7 +112,6 @@ public class Handler extends URLStreamHandler {
if (this.fallbackHandler != null) {
return this.fallbackHandler;
}
for (String handlerClassName : FALLBACK_HANDLERS) {
try {
Class<?> handlerClass = Class.forName(handlerClassName);
@ -135,24 +135,14 @@ public class Handler extends URLStreamHandler {
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();
int separatorIndex = spec.indexOf(SEPARATOR);
if (separatorIndex == -1) {
throw new MalformedURLException("Jar URL does not contain !/ separator");
}
JarFile jar = null;
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;
String name = spec.substring(0, separatorIndex);
return getRootJarFile(name);
}
private JarFile getRootJarFile(String name) throws IOException {
@ -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.
* @param sourceFile the source file to add

@ -93,7 +93,13 @@ public final class JarEntryData {
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) {
// aspectjrt-1.7.4.jar has a different ext bytes length in the
// local directory to the central directory. We need to re-read

@ -61,8 +61,6 @@ class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<Boolean>();
private final String jarFileUrlSpec;
private final JarFile jarFile;
private JarEntryData jarEntryData;
@ -71,19 +69,26 @@ class JarURLConnection extends java.net.JarURLConnection {
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
super(EMPTY_JAR_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;
String spec = url.getFile();
int separator = spec.lastIndexOf(SEPARATOR);
if (separator == -1) {
throw new MalformedURLException("no " + SEPARATOR + " found in url spec:"
+ spec);
this.jarEntryName = getJarEntryName(spec);
}
private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(name);
if (jarEntry == null) {
throwFileNotFound(jarEntry, jarFile);
}
this.jarFileUrlSpec = spec.substring(0, separator);
this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
return jarFile.getNestedJarFile(jarEntry);
}
private JarEntryName getJarEntryName(String spec) {
@ -99,16 +104,20 @@ class JarURLConnection extends java.net.JarURLConnection {
this.jarEntryData = this.jarFile.getJarEntryData(this.jarEntryName
.asAsciiBytes());
if (this.jarEntryData == null) {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + this.jarEntryName
+ " not found in " + this.jarFile.getName());
throwFileNotFound(this.jarEntryName, this.jarFile);
}
}
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
public Manifest getManifest() throws IOException {
try {
@ -135,10 +144,14 @@ class JarURLConnection extends java.net.JarURLConnection {
private URL buildJarFileUrl() {
try {
if (this.jarFileUrlSpec.indexOf(SEPARATOR) == -1) {
return new URL(this.jarFileUrlSpec);
String spec = this.jarFile.getUrl().getFile();
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) {
throw new IllegalStateException(ex);

@ -16,21 +16,32 @@
package org.springframework.boot.loader;
import java.io.File;
import java.net.URL;
import org.junit.Rule;
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.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link LaunchedURLClassLoader}.
*
* @author Dave Syer
* @author Phillip Webb
*/
@SuppressWarnings("resource")
public class LaunchedURLClassLoaderTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void resolveResourceFromWindowsFilesystem() throws Exception {
// This path is invalid - it should return null even on Windows.
@ -76,4 +87,17 @@ public class LaunchedURLClassLoaderTests {
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;
public abstract class TestJarCreator {
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);
JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream);
try {
@ -50,6 +54,9 @@ public abstract class TestJarCreator {
byte[] nestedJarData = getNestedJarData();
nestedEntry.setSize(nestedJarData.length);
nestedEntry.setCompressedSize(nestedJarData.length);
if (unpackNested) {
nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000");
}
CRC32 crc32 = new CRC32();
crc32.update(nestedJarData);
nestedEntry.setCrc(crc32.getValue());

@ -29,7 +29,9 @@ import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.util.AsciiBytes;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
@ -48,8 +50,12 @@ public class JarFileArchiveTests {
@Before
public void setup() throws Exception {
setup(false);
}
private void setup(boolean unpackNested) throws Exception {
this.rootJarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.rootJarFile);
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
this.archive = new JarFileArchive(this.rootJarFile);
}
@ -80,6 +86,15 @@ public class JarFileArchiveTests {
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
public void getFilteredArchive() throws Exception {
Archive filteredArchive = this.archive

@ -408,4 +408,16 @@ public class JarFileTests {
getEntries();
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();
}
}

@ -0,0 +1,65 @@
<?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>

@ -0,0 +1,8 @@
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

@ -0,0 +1,28 @@
/*
* 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 @@
package org.springframework.boot.maven;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
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.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope;
@ -46,8 +49,11 @@ public class ArtifactsLibraries implements Libraries {
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.unpacks = unpacks;
}
@Override
@ -55,8 +61,21 @@ public class ArtifactsLibraries implements Libraries {
for (Artifact artifact : this.artifacts) {
LibraryScope scope = SCOPES.get(artifact.getScope());
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;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
@ -108,6 +110,13 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
@Parameter
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
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters());
Libraries libraries = new ArtifactsLibraries(artifacts);
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack);
try {
repackager.repackage(target, libraries);
}

@ -21,13 +21,19 @@ import java.util.Collections;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback;
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.Mockito.verify;
@ -36,7 +42,7 @@ import static org.mockito.Mockito.verify;
*
* @author Phillip Webb
*/
public class ArtifactsLibrariesTest {
public class ArtifactsLibrariesTests {
@Mock
private Artifact artifact;
@ -50,11 +56,14 @@ public class ArtifactsLibrariesTest {
@Mock
private LibraryCallback callback;
@Captor
private ArgumentCaptor<Library> libraryCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
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);
}
@ -63,6 +72,25 @@ public class ArtifactsLibrariesTest {
given(this.artifact.getType()).willReturn("jar");
given(this.artifact.getScope()).willReturn("compile");
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 {
}
}
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) {
return this.content.containsKey(entry);
}

Loading…
Cancel
Save