diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index c9a7475a1f..513bc5cab7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -154,6 +154,11 @@ final class JarUrlConnection extends java.net.JarURLConnection { return guessContentTypeFromName(this.entryName); } + @Override + public long getLastModified() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getLastModified() : super.getLastModified(); + } + @Override public String getHeaderField(String name) { return (this.jarFileConnection != null) ? this.jarFileConnection.getHeaderField(name) : null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java index 32051123ab..0409fe6e3d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java @@ -28,6 +28,15 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.security.Permission; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import org.springframework.boot.loader.ref.Cleaner; @@ -39,6 +48,9 @@ import org.springframework.boot.loader.ref.Cleaner; */ class NestedUrlConnection extends URLConnection { + private static final DateTimeFormatter RFC_1123_DATE_TIME = DateTimeFormatter.RFC_1123_DATE_TIME + .withZone(ZoneId.of("GMT")); + private static final String CONTENT_TYPE = "x-java/jar"; private final NestedUrlConnectionResources resources; @@ -49,6 +61,8 @@ class NestedUrlConnection extends URLConnection { private FilePermission permission; + private Map> headerFields; + NestedUrlConnection(URL url) throws MalformedURLException { this(url, Cleaner.instance); } @@ -69,6 +83,60 @@ class NestedUrlConnection extends URLConnection { } } + @Override + public String getHeaderField(String name) { + List values = getHeaderFields().get(name); + return (values != null && !values.isEmpty()) ? values.get(0) : null; + } + + @Override + public String getHeaderField(int n) { + Entry> entry = getHeaderEntry(n); + List values = (entry != null) ? entry.getValue() : null; + return (values != null && !values.isEmpty()) ? values.get(0) : null; + } + + @Override + public String getHeaderFieldKey(int n) { + Entry> entry = getHeaderEntry(n); + return (entry != null) ? entry.getKey() : null; + } + + private Entry> getHeaderEntry(int n) { + Iterator>> iterator = getHeaderFields().entrySet().iterator(); + Entry> entry = null; + for (int i = 0; i < n; i++) { + entry = (!iterator.hasNext()) ? null : iterator.next(); + } + return entry; + } + + @Override + public Map> getHeaderFields() { + try { + connect(); + } + catch (IOException ex) { + return Collections.emptyMap(); + } + Map> headerFields = this.headerFields; + if (headerFields == null) { + headerFields = new LinkedHashMap<>(); + long contentLength = getContentLengthLong(); + long lastModified = getLastModified(); + if (contentLength > 0) { + headerFields.put("content-length", List.of(String.valueOf(contentLength))); + } + if (getLastModified() > 0) { + headerFields.put("last-modified", + List.of(RFC_1123_DATE_TIME.format(Instant.ofEpochMilli(lastModified)))); + } + headerFields = Collections.unmodifiableMap(headerFields); + this.headerFields = headerFields; + } + return headerFields; + } + @Override public int getContentLength() { long contentLength = getContentLengthLong(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java index e84ab43521..aa91510356 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -27,6 +27,8 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.security.Permission; +import java.time.Instant; +import java.time.temporal.ChronoField; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; @@ -490,4 +492,22 @@ class JarUrlConnectionTests { assertThat(connection).isNotNull(); } + @Test // gh-38204 + void getLastModifiedReturnsFileModifiedTime() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThat(connection.getLastModified()).isEqualTo(this.file.lastModified()); + } + + @Test // gh-38204 + void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection fileConnection = this.file.toURI().toURL().openConnection(); + assertThat(connection.getHeaderFieldDate("last-modified", 0)).isEqualTo(withoutNanos(this.file.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + + private long withoutNanos(long epochMilli) { + return Instant.ofEpochMilli(epochMilli).with(ChronoField.NANO_OF_SECOND, 0).toEpochMilli(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java index 7efcf2b257..488a168523 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java @@ -18,11 +18,15 @@ package org.springframework.boot.loader.net.protocol.nested; import java.io.File; import java.io.FilePermission; +import java.io.IOException; import java.io.InputStream; import java.lang.ref.Cleaner.Cleanable; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; import java.security.Permission; +import java.time.Instant; +import java.time.temporal.ChronoField; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -148,4 +152,23 @@ class NestedUrlConnectionTests { actionCaptor.getValue().run(); } + @Test // gh-38204 + void getLastModifiedReturnsFileModifiedTime() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + assertThat(connection.getLastModified()).isEqualTo(this.jarFile.lastModified()); + } + + @Test // gh-38204 + void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + URLConnection fileConnection = this.jarFile.toURI().toURL().openConnection(); + assertThat(connection.getHeaderFieldDate("last-modified", 0)) + .isEqualTo(withoutNanos(this.jarFile.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + + private long withoutNanos(long epochMilli) { + return Instant.ofEpochMilli(epochMilli).with(ChronoField.NANO_OF_SECOND, 0).toEpochMilli(); + } + }