diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java index e59f44c80a..2ef17ba199 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java @@ -22,6 +22,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributeView; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; @@ -96,7 +97,13 @@ class ExtractCommand extends Command { try (OutputStream out = new FileOutputStream(file)) { StreamUtils.copy(zip, out); } - Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime()); + try { + Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class) + .setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime()); + } + catch (IOException ex) { + // File system does not support setting time attributes. Continue. + } } private void mkParentDirs(File file) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java index 114ecb0de9..bc098be1b4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java @@ -21,11 +21,18 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.jar.JarEntry; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -52,6 +59,12 @@ import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) class ExtractCommandTests { + private static final FileTime CREATION_TIME = FileTime.from(Instant.now().minus(3, ChronoUnit.DAYS)); + + private static final FileTime LAST_MODIFIED_TIME = FileTime.from(Instant.now().minus(2, ChronoUnit.DAYS)); + + private static final FileTime LAST_ACCESS_TIME = FileTime.from(Instant.now().minus(1, ChronoUnit.DAYS)); + @TempDir File temp; @@ -80,13 +93,32 @@ class ExtractCommandTests { given(this.context.getWorkingDir()).willReturn(this.extract); this.command.run(Collections.emptyMap(), Collections.emptyList()); assertThat(this.extract.list()).containsOnly("a", "b", "c", "d"); - assertThat(new File(this.extract, "a/a/a.jar")).exists(); - assertThat(new File(this.extract, "b/b/b.jar")).exists(); - assertThat(new File(this.extract, "c/c/c.jar")).exists(); + assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes); + assertThat(new File(this.extract, "b/b/b.jar")).exists().satisfies(this::timeAttributes); + assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes); assertThat(new File(this.extract, "d")).isDirectory(); assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist(); } + private void timeAttributes(File file) { + try { + BasicFileAttributes basicAttributes = Files + .getFileAttributeView(file.toPath(), BasicFileAttributeView.class, new LinkOption[0]) + .readAttributes(); + assertThat(basicAttributes.lastModifiedTime().to(TimeUnit.SECONDS)) + .isEqualTo(LAST_MODIFIED_TIME.to(TimeUnit.SECONDS)); + assertThat(basicAttributes.creationTime().to(TimeUnit.SECONDS)).satisfiesAnyOf( + (creationTime) -> assertThat(creationTime).isEqualTo(CREATION_TIME.to(TimeUnit.SECONDS)), + // On macOS (at least) the creation time is the last modified time + (creationTime) -> assertThat(creationTime).isEqualTo(LAST_MODIFIED_TIME.to(TimeUnit.SECONDS))); + assertThat(basicAttributes.lastAccessTime().to(TimeUnit.SECONDS)) + .isEqualTo(LAST_ACCESS_TIME.to(TimeUnit.SECONDS)); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + @Test void runWhenHasDestinationOptionExtractsLayers() { given(this.context.getArchiveFile()).willReturn(this.jarFile); @@ -94,9 +126,9 @@ class ExtractCommandTests { this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()), Collections.emptyList()); assertThat(this.extract.list()).containsOnly("out"); - assertThat(new File(this.extract, "out/a/a/a.jar")).exists(); - assertThat(new File(this.extract, "out/b/b/b.jar")).exists(); - assertThat(new File(this.extract, "out/c/c/c.jar")).exists(); + assertThat(new File(this.extract, "out/a/a/a.jar")).exists().satisfies(this::timeAttributes); + assertThat(new File(this.extract, "out/b/b/b.jar")).exists().satisfies(this::timeAttributes); + assertThat(new File(this.extract, "out/c/c/c.jar")).exists().satisfies(this::timeAttributes); } @Test @@ -105,8 +137,8 @@ class ExtractCommandTests { given(this.context.getWorkingDir()).willReturn(this.extract); this.command.run(Collections.emptyMap(), Arrays.asList("a", "c")); assertThat(this.extract.list()).containsOnly("a", "c"); - assertThat(new File(this.extract, "a/a/a.jar")).exists(); - assertThat(new File(this.extract, "c/c/c.jar")).exists(); + assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes); + assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes); assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist(); } @@ -148,21 +180,21 @@ class ExtractCommandTests { private File createJarFile(String name, Consumer streamHandler) throws Exception { File file = new File(this.temp, name); try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) { - out.putNextEntry(new ZipEntry("a/")); + out.putNextEntry(entry("a/")); out.closeEntry(); - out.putNextEntry(new ZipEntry("a/a.jar")); + out.putNextEntry(entry("a/a.jar")); out.closeEntry(); - out.putNextEntry(new ZipEntry("b/")); + out.putNextEntry(entry("b/")); out.closeEntry(); - out.putNextEntry(new ZipEntry("b/b.jar")); + out.putNextEntry(entry("b/b.jar")); out.closeEntry(); - out.putNextEntry(new ZipEntry("c/")); + out.putNextEntry(entry("c/")); out.closeEntry(); - out.putNextEntry(new ZipEntry("c/c.jar")); + out.putNextEntry(entry("c/c.jar")); out.closeEntry(); - out.putNextEntry(new ZipEntry("d/")); + out.putNextEntry(entry("d/")); out.closeEntry(); - out.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); + out.putNextEntry(entry("META-INF/MANIFEST.MF")); out.write(getFile("test-manifest.MF").getBytes()); out.closeEntry(); streamHandler.accept(out); @@ -170,6 +202,14 @@ class ExtractCommandTests { return file; } + private ZipEntry entry(String path) { + ZipEntry entry = new ZipEntry(path); + entry.setCreationTime(CREATION_TIME); + entry.setLastModifiedTime(LAST_MODIFIED_TIME); + entry.setLastAccessTime(LAST_ACCESS_TIME); + return entry; + } + private String getFile(String fileName) throws Exception { ClassPathResource resource = new ClassPathResource(fileName, getClass()); InputStreamReader reader = new InputStreamReader(resource.getInputStream());