diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index b104e39312..64aed13e31 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -183,15 +183,15 @@ class BootZipCopyAction implements CopyAction { */ private class Processor { - private ZipArchiveOutputStream out; + private final ZipArchiveOutputStream out; private final LayersIndex layerIndex; private LoaderZipEntries.WrittenEntries writtenLoaderEntries; - private Set writtenDirectories = new LinkedHashSet<>(); + private final Set writtenDirectories = new LinkedHashSet<>(); - private Set writtenLibraries = new LinkedHashSet<>(); + private final Set writtenLibraries = new LinkedHashSet<>(); Processor(ZipArchiveOutputStream out) { this.out = out; @@ -224,11 +224,8 @@ class BootZipCopyAction implements CopyAction { private void processDirectory(FileCopyDetails details) throws IOException { String name = details.getRelativePath().getPathString(); - long time = getTime(details); - writeParentDirectoriesIfNecessary(name, time); ZipArchiveEntry entry = new ZipArchiveEntry(name + '/'); - entry.setUnixMode(UnixStat.DIR_FLAG | details.getMode()); - entry.setTime(time); + prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode()); this.out.putArchiveEntry(entry); this.out.closeArchiveEntry(); this.writtenDirectories.add(name); @@ -236,11 +233,8 @@ class BootZipCopyAction implements CopyAction { private void processFile(FileCopyDetails details) throws IOException { String name = details.getRelativePath().getPathString(); - long time = getTime(details); - writeParentDirectoriesIfNecessary(name, time); ZipArchiveEntry entry = new ZipArchiveEntry(name); - entry.setUnixMode(UnixStat.FILE_FLAG | details.getMode()); - entry.setTime(time); + prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode()); ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); if (compression == ZipCompression.STORED) { prepareStoredEntry(details, entry); @@ -257,13 +251,11 @@ class BootZipCopyAction implements CopyAction { } } - private void writeParentDirectoriesIfNecessary(String name, long time) throws IOException { + private void writeParentDirectoriesIfNecessary(String name, Long time) throws IOException { String parentDirectory = getParentDirectory(name); if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) { - writeParentDirectoriesIfNecessary(parentDirectory, time); ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/'); - entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); - entry.setTime(time); + prepareEntry(entry, parentDirectory, time, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); this.out.putArchiveEntry(entry); this.out.closeArchiveEntry(); } @@ -293,8 +285,7 @@ class BootZipCopyAction implements CopyAction { // Don't write loader entries until after META-INF folder (see gh-16698) return; } - LoaderZipEntries loaderEntries = new LoaderZipEntries( - BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES); + LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime()); this.writtenLoaderEntries = loaderEntries.writeTo(this.out); if (BootZipCopyAction.this.layerResolver != null) { for (String name : this.writtenLoaderEntries.getFiles()) { @@ -320,7 +311,7 @@ class BootZipCopyAction implements CopyAction { private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException { String name = location + library.getName(); - writeEntry(name, ZipEntryWriter.fromInputStream(library.openStream()), false, + writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false, (entry) -> prepareStoredEntry(library.openStream(), entry)); if (BootZipCopyAction.this.layerResolver != null) { Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library); @@ -328,29 +319,14 @@ class BootZipCopyAction implements CopyAction { } } - private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { - prepareStoredEntry(details.open(), archiveEntry); - if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { - archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); - } - } - - private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException { - archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); - Crc32OutputStream crcStream = new Crc32OutputStream(); - int size = FileCopyUtils.copy(input, crcStream); - archiveEntry.setSize(size); - archiveEntry.setCompressedSize(size); - archiveEntry.setCrc(crcStream.getCrc()); - } - private void writeClassPathIndexIfNecessary() throws IOException { Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index"); if (classPathIndex != null) { List lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"") .collect(Collectors.toList()); - writeEntry(classPathIndex, ZipEntryWriter.fromLines(BootZipCopyAction.this.encoding, lines), true); + writeEntry(classPathIndex, ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines), + true); } } @@ -361,23 +337,22 @@ class BootZipCopyAction implements CopyAction { Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute"); Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); this.layerIndex.add(layer, name); - writeEntry(name, (entry, out) -> this.layerIndex.writeTo(out), false); + writeEntry(name, this.layerIndex::writeTo, false); } } - private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex) throws IOException { + private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex) + throws IOException { writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE); } - private void writeEntry(String name, ZipEntryWriter entryWriter, boolean addToLayerIndex, + private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex, ZipEntryCustomizer entryCustomizer) throws IOException { - writeParentDirectoriesIfNecessary(name, CONSTANT_TIME_FOR_ZIP_ENTRIES); ZipArchiveEntry entry = new ZipArchiveEntry(name); - entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); - entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES); + prepareEntry(entry, name, getTime(), UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); entryCustomizer.customize(entry); this.out.putArchiveEntry(entry); - entryWriter.writeTo(entry, this.out); + entryWriter.writeTo(this.out); this.out.closeArchiveEntry(); if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) { Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); @@ -385,9 +360,42 @@ class BootZipCopyAction implements CopyAction { } } - private long getTime(FileCopyDetails details) { - return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified() - : CONSTANT_TIME_FOR_ZIP_ENTRIES; + private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mode) throws IOException { + writeParentDirectoriesIfNecessary(name, time); + entry.setUnixMode(mode); + if (time != null) { + entry.setTime(time); + } + } + + private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { + prepareStoredEntry(details.open(), archiveEntry); + if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { + archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); + } + } + + private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException { + archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); + Crc32OutputStream crcStream = new Crc32OutputStream(); + int size = FileCopyUtils.copy(input, crcStream); + archiveEntry.setSize(size); + archiveEntry.setCompressedSize(size); + archiveEntry.setCrc(crcStream.getCrc()); + } + + private Long getTime() { + return getTime(null); + } + + private Long getTime(FileCopyDetails details) { + if (!BootZipCopyAction.this.preserveFileTimestamps) { + return CONSTANT_TIME_FOR_ZIP_ENTRIES; + } + if (details != null) { + return details.getLastModified(); + } + return null; } } @@ -414,41 +422,40 @@ class BootZipCopyAction implements CopyAction { * Callback used to write a zip entry data. */ @FunctionalInterface - private interface ZipEntryWriter { + private interface ZipEntryContentWriter { /** * Write the entry data. - * @param entry the entry being written * @param out the output stream used to write the data * @throws IOException on IO error */ - void writeTo(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException; + void writeTo(ZipArchiveOutputStream out) throws IOException; /** - * Create a new {@link ZipEntryWriter} that will copy content from the given - * {@link InputStream}. + * Create a new {@link ZipEntryContentWriter} that will copy content from the + * given {@link InputStream}. * @param in the source input stream - * @return a new {@link ZipEntryWriter} instance + * @return a new {@link ZipEntryContentWriter} instance */ - static ZipEntryWriter fromInputStream(InputStream in) { - return (entry, out) -> { + static ZipEntryContentWriter fromInputStream(InputStream in) { + return (out) -> { StreamUtils.copy(in, out); in.close(); }; } /** - * Create a new {@link ZipEntryWriter} that will copy content from the given - * lines. + * Create a new {@link ZipEntryContentWriter} that will copy content from the + * given lines. * @param encoding the required character encoding * @param lines the lines to write - * @return a new {@link ZipEntryWriter} instance + * @return a new {@link ZipEntryContentWriter} instance */ - static ZipEntryWriter fromLines(String encoding, Collection lines) { - return (entry, out) -> { + static ZipEntryContentWriter fromLines(String encoding, Collection lines) { + return (out) -> { OutputStreamWriter writer = new OutputStreamWriter(out, encoding); for (String line : lines) { - writer.append(line + "\n"); + writer.append(line).append("\n"); } writer.flush(); }; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index b7caad0638..4fd6d854ff 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -29,11 +29,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; +import org.springframework.util.StreamUtils; + /** * Internal utility used to copy entries from the {@code spring-boot-loader.jar}. * * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class LoaderZipEntries { @@ -84,11 +87,7 @@ class LoaderZipEntries { } private void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[4096]; - int bytesRead = -1; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } + StreamUtils.copy(in, out); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 527d2becf4..8a7a2fc7a4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -40,6 +40,7 @@ import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.gradle.api.Project; +import org.gradle.api.internal.file.archive.ZipCopyAction; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.gradle.api.tasks.bundling.Jar; import org.gradle.testfixtures.ProjectBuilder; @@ -56,6 +57,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @param the type of the concrete BootArchive implementation * @author Andy Wilkinson + * @author Scott Frederick */ abstract class AbstractBootArchiveTests { @@ -330,6 +332,12 @@ abstract class AbstractBootArchiveTests { } } + @Test + void constantTimestampMatchesGradleInternalTimestamp() { + assertThat(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES) + .isEqualTo(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); + } + @Test void reproducibleOrderingCanBeEnabled() throws IOException { this.task.setMainClassName("com.example.Main"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index 4aa69f83eb..60a8b841c7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -101,14 +101,10 @@ class BootJarTests extends AbstractBootArchiveTests { assertThat(entryNames).contains("BOOT-INF/lib/first-library.jar", "BOOT-INF/lib/second-library.jar", "BOOT-INF/lib/third-library-SNAPSHOT.jar", "BOOT-INF/classes/com/example/Application.class", "BOOT-INF/classes/application.properties", "BOOT-INF/classes/static/test.css"); - ZipEntry layersIndexEntry = jarFile.getEntry("BOOT-INF/layers.idx"); - assertThat(layersIndexEntry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); List index = entryLines(jarFile, "BOOT-INF/layers.idx"); assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); String layerToolsJar = "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName(); - ZipEntry layerToolsEntry = jarFile.getEntry(layerToolsJar); - assertThat(layerToolsEntry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); List expected = new ArrayList<>(); expected.add("- \"dependencies\":"); expected.add(" - \"BOOT-INF/lib/first-library.jar\"");