Commit a6d24a3b authored by Andy Wilkinson's avatar Andy Wilkinson

Allow bootWar to package webapp resources in dirs that overlap loader

Previously, the presence of a src/main/webapp/org directory would
cause the execution of BootWar to fail due to an attempt to write
a duplicate org/ entry to the zip output stream.

This commit updates BootZipCopyAction so that FileTreeElements that
match a directory entry created while writing the loader classes are
skipped.

Closes gh-8972
parent 15b9707f
...@@ -20,6 +20,8 @@ import java.io.File; ...@@ -20,6 +20,8 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
...@@ -34,6 +36,7 @@ import org.gradle.api.internal.file.copy.CopyAction; ...@@ -34,6 +36,7 @@ import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream; import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.specs.Specs;
import org.gradle.api.tasks.WorkResult; import org.gradle.api.tasks.WorkResult;
import org.gradle.util.GUtil; import org.gradle.util.GUtil;
...@@ -78,19 +81,20 @@ class BootZipCopyAction implements CopyAction { ...@@ -78,19 +81,20 @@ class BootZipCopyAction implements CopyAction {
@Override @Override
public WorkResult execute(CopyActionProcessingStream stream) { public WorkResult execute(CopyActionProcessingStream stream) {
ZipOutputStream zipStream; ZipOutputStream zipStream;
Spec<FileTreeElement> loaderEntries;
try { try {
FileOutputStream fileStream = new FileOutputStream(this.output); FileOutputStream fileStream = new FileOutputStream(this.output);
writeLaunchScriptIfNecessary(fileStream); writeLaunchScriptIfNecessary(fileStream);
zipStream = new ZipOutputStream(fileStream); zipStream = new ZipOutputStream(fileStream);
writeLoaderClassesIfNecessary(zipStream); loaderEntries = writeLoaderClassesIfNecessary(zipStream);
} }
catch (IOException ex) { catch (IOException ex) {
throw new GradleException("Failed to create " + this.output, ex); throw new GradleException("Failed to create " + this.output, ex);
} }
try { try {
stream.process(new ZipStreamAction(zipStream, this.output, stream.process(new ZipStreamAction(zipStream, this.output,
this.preserveFileTimestamps, this.requiresUnpack, this.exclusions, this.preserveFileTimestamps, this.requiresUnpack,
this.compressionResolver)); createExclusionSpec(loaderEntries), this.compressionResolver));
} }
finally { finally {
try { try {
...@@ -103,24 +107,40 @@ class BootZipCopyAction implements CopyAction { ...@@ -103,24 +107,40 @@ class BootZipCopyAction implements CopyAction {
return () -> true; return () -> true;
} }
private void writeLoaderClassesIfNecessary(ZipOutputStream out) { @SuppressWarnings("unchecked")
if (this.includeDefaultLoader) { private Spec<FileTreeElement> createExclusionSpec(
writeLoaderClasses(out); Spec<FileTreeElement> loaderEntries) {
return Specs.union(loaderEntries, this.exclusions);
}
private Spec<FileTreeElement> writeLoaderClassesIfNecessary(ZipOutputStream out) {
if (!this.includeDefaultLoader) {
return Specs.satisfyNone();
} }
return writeLoaderClasses(out);
} }
private void writeLoaderClasses(ZipOutputStream out) { private Spec<FileTreeElement> writeLoaderClasses(ZipOutputStream out) {
ZipEntry entry;
try (ZipInputStream in = new ZipInputStream(getClass() try (ZipInputStream in = new ZipInputStream(getClass()
.getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { .getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
Set<String> entries = new HashSet<String>();
ZipEntry entry;
while ((entry = in.getNextEntry()) != null) { while ((entry = in.getNextEntry()) != null) {
if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) { if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
writeDirectory(entry, out); writeDirectory(entry, out);
entries.add(entry.getName());
} }
if (entry.getName().endsWith(".class")) { else if (entry.getName().endsWith(".class")) {
writeClass(entry, in, out); writeClass(entry, in, out);
} }
} }
return (element) -> {
String path = element.getRelativePath().getPathString();
if (element.isDirectory() && !path.endsWith(("/"))) {
path += "/";
}
return entries.contains(path);
};
} }
catch (IOException ex) { catch (IOException ex) {
throw new GradleException("Failed to write loader classes", ex); throw new GradleException("Failed to write loader classes", ex);
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package org.springframework.boot.gradle.tasks.bundling; package org.springframework.boot.gradle.tasks.bundling;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.jar.JarFile; import java.util.jar.JarFile;
...@@ -76,4 +77,21 @@ public class BootWarTests extends AbstractBootArchiveTests<BootWar> { ...@@ -76,4 +77,21 @@ public class BootWarTests extends AbstractBootArchiveTests<BootWar> {
} }
} }
@Test
public void webappResourcesInDirectoriesThatOverlapWithLoaderCanBePackaged()
throws IOException {
File webappFolder = this.temp.newFolder("src", "main", "webapp");
File orgFolder = new File(webappFolder, "org");
orgFolder.mkdir();
new File(orgFolder, "foo.txt").createNewFile();
getTask().from(webappFolder);
getTask().setMainClass("com.example.Main");
getTask().execute();
assertThat(getTask().getArchivePath().exists());
try (JarFile jarFile = new JarFile(getTask().getArchivePath())) {
assertThat(jarFile.getEntry("org/")).isNotNull();
assertThat(jarFile.getEntry("org/foo.txt")).isNotNull();
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment