diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java
index 89825113db..eb2be18dd1 100644
--- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java
+++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java
@@ -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));
}
}
diff --git a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc
index 00f44f2ea2..9a88a544a5 100644
--- a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc
+++ b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc
@@ -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));
}
});
----
diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc
index 48f685795c..cd9508c0e7 100644
--- a/spring-boot-docs/src/main/asciidoc/howto.adoc
+++ b/spring-boot-docs/src/main/asciidoc/howto.adoc
@@ -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"]
+----
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.jruby
+ jruby-complete
+
+
+
+
+
+
+----
+
+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
diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy
index 1aa5c0df0e..bac7f934ef 100644
--- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy
+++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy
@@ -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 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;
+
}
diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java
index 0f9caf9b65..0394ef61a3 100644
--- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java
+++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java
@@ -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 custom = getArtifacts(this.customConfigurationName);
if (custom != null) {
libraries(LibraryScope.CUSTOM, custom, callback);
}
else {
- FileCollection compile = this.project.getConfigurations()
- .getByName("compile");
+ Set compile = getArtifacts("compile");
- FileCollection runtime = this.project.getConfigurations()
- .getByName("runtime");
- runtime = runtime.minus(compile);
-
- FileCollection provided = this.project.getConfigurations()
- .findByName(this.providedConfigurationName);
+ Set runtime = getArtifacts("runtime");
+ runtime = minus(runtime, compile);
+ Set 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 getArtifacts(String configurationName) {
+ Configuration configuration = (configurationName == null ? null : this.project
+ .getConfigurations().findByName(configurationName));
+ return (configuration == null ? null : configuration.getResolvedConfiguration()
+ .getResolvedArtifacts());
+ }
+
+ private Set minus(Set source,
+ Set toRemove) {
+ if (source == null || toRemove == null) {
+ return source;
+ }
+ Set filesToRemove = new HashSet();
+ for (ResolvedArtifact artifact : toRemove) {
+ filesToRemove.add(artifact.getFile());
+ }
+ Set result = new LinkedHashSet();
+ for (ResolvedArtifact artifact : source) {
+ if (!toRemove.contains(artifact.getFile())) {
+ result.add(artifact);
+ }
+ }
+ return result;
+ }
+
+ private void libraries(LibraryScope scope, Set 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;
+ }
+
}
diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java
index 96c3fa528a..8d8079ae7a 100644
--- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java
+++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackagePluginFeatures.java
@@ -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);
+ }
+ }
+
}
}
diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java
index 6b0bcbbee0..f083a5f84f 100644
--- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java
+++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java
@@ -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 files = new ArrayList();
- 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());
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java
index cdf1d543f5..e7dcbdd9c1 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java
@@ -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();
+ }
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java
index dbef42207f..55daa0a303 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java
@@ -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));
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java
new file mode 100644
index 0000000000..4fa9a9d818
--- /dev/null
+++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCallback.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCallback.java
index 7b2bdd4caa..1aeff158a6 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCallback.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCallback.java
@@ -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;
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java
index 3f3a815e84..785d6a2c0e 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java
@@ -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);
}
}
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java
index 475f16e2a2..4fe75b3303 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java
@@ -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"));
+ }
+
}
diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java
index 49a42e502e..042be2570b 100644
--- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java
+++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java
@@ -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();
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
index f55aff6821..4a3a944d21 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java
@@ -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 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() {
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java
index 25569b2b5c..a9eeb8b646 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java
@@ -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
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java
index 1a336d93eb..9f0bca4801 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java
@@ -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
diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java
index e4320e3070..fb4c7acd38 100644
--- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java
+++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java
@@ -61,8 +61,6 @@ class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal useFastExceptions = new ThreadLocal();
- 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;
- 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);
+ 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.jarFileUrlSpec = spec.substring(0, separator);
- this.jarEntryName = getJarEntryName(spec.substring(separator + 2));
+ this.jarFile = jarFile;
+ this.jarEntryName = getJarEntryName(spec);
+ }
+
+ private JarFile getNestedJarFile(JarFile jarFile, String name) throws IOException {
+ JarEntry jarEntry = jarFile.getJarEntry(name);
+ if (jarEntry == null) {
+ throwFileNotFound(jarEntry, jarFile);
+ }
+ 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());
}
- return new URL("jar:" + this.jarFileUrlSpec);
+ if (spec.indexOf(SEPARATOR) == -1) {
+ return new URL(spec);
+ }
+ return new URL("jar:" + spec);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
index b453c5851c..2fa10b5d87 100644
--- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
+++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java
@@ -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));
+ }
+
}
diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java
index 2c100efbf9..f113f6d0ab 100644
--- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java
+++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java
@@ -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());
diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java
index 7bd767dcf4..ff8f7e1a38 100644
--- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java
+++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java
@@ -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
diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java
index 81edda650e..a133b7ee82 100644
--- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java
+++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java
@@ -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();
+ }
+
}
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml
new file mode 100644
index 0000000000..ae5eeffbb8
--- /dev/null
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+ org.springframework.boot.maven.it
+ jar-with-unpack
+ 0.0.1.BUILD-SNAPSHOT
+
+ UTF-8
+
+
+
+
+ @project.groupId@
+ @project.artifactId@
+ @project.version@
+
+
+
+ repackage
+
+
+
+
+ org.springframework
+ spring-core
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.4
+
+
+
+ Foo
+
+
+
+
+
+
+
+
+ org.springframework
+ spring-context
+ 4.0.5.RELEASE
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.0.1
+ provided
+
+
+ log4j
+ log4j
+ 1.2.17
+
+
+
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java
new file mode 100644
index 0000000000..0b3b431677
--- /dev/null
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java
@@ -0,0 +1,8 @@
+package org.test;
+
+public class SampleApplication {
+
+ public static void main(String[] args) {
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy
new file mode 100644
index 0000000000..9abbeb593d
--- /dev/null
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy
@@ -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();
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java
index 2e672703e2..56d027faa4 100644
--- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java
@@ -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 artifacts;
- public ArtifactsLibraries(Set artifacts) {
+ private final Collection unpacks;
+
+ public ArtifactsLibraries(Set artifacts, Collection 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;
+ }
}
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
index 8d45615272..4d80c9f940 100644
--- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java
@@ -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 requiresUnpack;
+
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Set artifacts = filterDependencies(this.project.getArtifacts(),
getFilters());
- Libraries libraries = new ArtifactsLibraries(artifacts);
+ Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack);
try {
repackager.repackage(target, libraries);
}
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTest.java b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java
similarity index 56%
rename from spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTest.java
rename to spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java
index 3faf8f3a48..15c05f33f3 100644
--- a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTest.java
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java
@@ -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 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));
}
}
diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java
index f93218cc59..84e11f692a 100644
--- a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java
+++ b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java
@@ -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);
}