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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user