Add support for unpacking nested JARs

Update the executable JAR code to automatically unpack any entries
which include an entry comment starting `UNPACK:` to the temp folder.
The existing Maven and Gradle plugins have been updated with new
configuration options and the `spring-boot-tools` project has been
updated to write the appropriate entry comment based on a flag passed
in via the `Library` class.

This support has been added to allow libraries such a JRuby (which
assumes that `jruby-complete.jar` is always accessible as file) to work
with Spring Boot executable jars.

Fixes gh-1070
This commit is contained in:
Phillip Webb
2014-06-24 00:35:01 -07:00
parent 5f8fbfd73a
commit f30b962ff9
24 changed files with 477 additions and 39 deletions

View File

@@ -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();
}
}

View File

@@ -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));
}

View File

@@ -31,14 +31,27 @@ public class Library {
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;
}
/**
@@ -55,4 +68,12 @@ public class Library {
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;
}
}

View File

@@ -147,7 +147,7 @@ public class Repackager {
String destination = Repackager.this.layout
.getLibraryDestination(file.getName(), library.getScope());
if (destination != null) {
writer.writeNestedLibrary(destination, file);
writer.writeNestedLibrary(destination, library);
}
}
}

View File

@@ -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"));
}
}

View File

@@ -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);
@@ -267,11 +270,17 @@ public class RepackagerTests {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
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
@@ -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();