From 0caa2ac6964f7ac647b52d800e2a37259abdf116 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 3 Aug 2022 22:50:34 +0200 Subject: [PATCH] Customize connection in UrlResource getInputStream Prior to this commit, the `AbstractFileResolvingResource` would provide a default implementation for `customizeConnection` which only sets the HTTP request method as "HEAD". While this is consistent with its usage within that class (in `exists()`, `contentLength()` or `lastModified()`), this is not opened for general usage by sub-classes. `UrlResource` is an example of that, where its `getInputStream()` method does not call this customization method. This not only prevents implementations from calling `customizeConnection` in various cases, but it also misleads developers as they might think that customizations will be applied automatically. This commit ensures that `customizeConnection` is called in all relevant places and that the configuration of the HTTP method is instead done in each method as it is use case specific. Fixes gh-28909 --- spring-core/spring-core.gradle | 1 + .../io/AbstractFileResolvingResource.java | 19 +- .../springframework/core/io/UrlResource.java | 3 +- .../core/io/ResourceTests.java | 537 +++++++++++------- 4 files changed, 338 insertions(+), 222 deletions(-) diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 693d0ad2bc3..7d13980b9ba 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -58,6 +58,7 @@ dependencies { testImplementation("org.xmlunit:xmlunit-matchers") testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor.tools:blockhound") + testImplementation("com.squareup.okhttp3:mockwebserver") testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("org.junit.platform:junit-platform-launcher") testFixturesImplementation("org.junit.jupiter:junit-jupiter-api") diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java index 2c44b08e663..0661f8bcbb5 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -57,6 +57,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { HttpURLConnection httpCon = (con instanceof HttpURLConnection ? (HttpURLConnection) con : null); if (httpCon != null) { + httpCon.setRequestMethod("HEAD"); int code = httpCon.getResponseCode(); if (code == HttpURLConnection.HTTP_OK) { return true; @@ -108,6 +109,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { customizeConnection(con); if (con instanceof HttpURLConnection) { HttpURLConnection httpCon = (HttpURLConnection) con; + httpCon.setRequestMethod("HEAD"); int code = httpCon.getResponseCode(); if (code != HttpURLConnection.HTTP_OK) { httpCon.disconnect(); @@ -245,6 +247,10 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { // Try a URL connection content-length header URLConnection con = url.openConnection(); customizeConnection(con); + if (con instanceof HttpURLConnection) { + HttpURLConnection httpCon = (HttpURLConnection) con; + httpCon.setRequestMethod("HEAD"); + } return con.getContentLengthLong(); } } @@ -270,6 +276,10 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { // Try a URL connection last-modified header URLConnection con = url.openConnection(); customizeConnection(con); + if (con instanceof HttpURLConnection) { + HttpURLConnection httpCon = (HttpURLConnection) con; + httpCon.setRequestMethod("HEAD"); + } long lastModified = con.getLastModified(); if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) { throw new FileNotFoundException(getDescription() + @@ -279,8 +289,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { } /** - * Customize the given {@link URLConnection}, obtained in the course of an - * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. + * Customize the given {@link URLConnection} before fetching the resource. *

Calls {@link ResourceUtils#useCachesIfNecessary(URLConnection)} and * delegates to {@link #customizeConnection(HttpURLConnection)} if possible. * Can be overridden in subclasses. @@ -295,14 +304,12 @@ public abstract class AbstractFileResolvingResource extends AbstractResource { } /** - * Customize the given {@link HttpURLConnection}, obtained in the course of an - * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. - *

Sets request method "HEAD" by default. Can be overridden in subclasses. + * Customize the given {@link HttpURLConnection} before fetching the resource. + *

Can be overridden in subclasses for configuring request headers and timeouts. * @param con the HttpURLConnection to customize * @throws IOException if thrown from HttpURLConnection methods */ protected void customizeConnection(HttpURLConnection con) throws IOException { - con.setRequestMethod("HEAD"); } diff --git a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java index 53b38197e6c..2935dec92c4 100644 --- a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java @@ -28,7 +28,6 @@ import java.net.URLConnection; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -183,7 +182,7 @@ public class UrlResource extends AbstractFileResolvingResource { @Override public InputStream getInputStream() throws IOException { URLConnection con = this.url.openConnection(); - ResourceUtils.useCachesIfNecessary(con); + customizeConnection(con); try { return con.getInputStream(); } diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java index 5a9c8e5d6d9..4353f85e8a0 100644 --- a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -23,20 +23,32 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashSet; - +import java.util.stream.Stream; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * Unit tests for various {@link Resource} implementations. @@ -44,137 +56,25 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; * @author Juergen Hoeller * @author Chris Beams * @author Sam Brannen - * @since 09.09.2004 + * @author Brian Clozel */ class ResourceTests { - @Test - void byteArrayResource() throws IOException { - Resource resource = new ByteArrayResource("testString".getBytes()); - assertThat(resource.exists()).isTrue(); - assertThat(resource.isOpen()).isFalse(); - String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); - assertThat(content).isEqualTo("testString"); - assertThat(new ByteArrayResource("testString".getBytes())).isEqualTo(resource); - } - - @Test - void byteArrayResourceWithDescription() throws IOException { - Resource resource = new ByteArrayResource("testString".getBytes(), "my description"); - assertThat(resource.exists()).isTrue(); - assertThat(resource.isOpen()).isFalse(); - String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); - assertThat(content).isEqualTo("testString"); - assertThat(resource.getDescription().contains("my description")).isTrue(); - assertThat(new ByteArrayResource("testString".getBytes())).isEqualTo(resource); - } - - @Test - void inputStreamResource() throws IOException { - InputStream is = new ByteArrayInputStream("testString".getBytes()); - Resource resource = new InputStreamResource(is); - assertThat(resource.exists()).isTrue(); - assertThat(resource.isOpen()).isTrue(); - String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); - assertThat(content).isEqualTo("testString"); - assertThat(new InputStreamResource(is)).isEqualTo(resource); - } - - @Test - void inputStreamResourceWithDescription() throws IOException { - InputStream is = new ByteArrayInputStream("testString".getBytes()); - Resource resource = new InputStreamResource(is, "my description"); - assertThat(resource.exists()).isTrue(); - assertThat(resource.isOpen()).isTrue(); - String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); - assertThat(content).isEqualTo("testString"); - assertThat(resource.getDescription().contains("my description")).isTrue(); - assertThat(new InputStreamResource(is)).isEqualTo(resource); - } - - @Test - void classPathResource() throws IOException { - Resource resource = new ClassPathResource("org/springframework/core/io/Resource.class"); - doTestResource(resource); - Resource resource2 = new ClassPathResource("org/springframework/core/../core/io/./Resource.class"); - assertThat(resource2).isEqualTo(resource); - Resource resource3 = new ClassPathResource("org/springframework/core/").createRelative("../core/io/./Resource.class"); - assertThat(resource3).isEqualTo(resource); - - // Check whether equal/hashCode works in a HashSet. - HashSet resources = new HashSet<>(); - resources.add(resource); - resources.add(resource2); - assertThat(resources.size()).isEqualTo(1); - } - - @Test - void classPathResourceWithClassLoader() throws IOException { - Resource resource = - new ClassPathResource("org/springframework/core/io/Resource.class", getClass().getClassLoader()); - doTestResource(resource); - assertThat(new ClassPathResource("org/springframework/core/../core/io/./Resource.class", getClass().getClassLoader())).isEqualTo(resource); - } - - @Test - void classPathResourceWithClass() throws IOException { - Resource resource = new ClassPathResource("Resource.class", getClass()); - doTestResource(resource); - assertThat(new ClassPathResource("Resource.class", getClass())).isEqualTo(resource); - } - - @Test - void fileSystemResource() throws IOException { - String file = getClass().getResource("Resource.class").getFile(); - Resource resource = new FileSystemResource(file); - doTestResource(resource); - assertThat(resource).isEqualTo(new FileSystemResource(file)); - } - - @Test - void fileSystemResourceWithFile() throws IOException { - File file = new File(getClass().getResource("Resource.class").getFile()); - Resource resource = new FileSystemResource(file); - doTestResource(resource); - assertThat(resource).isEqualTo(new FileSystemResource(file)); - } - - @Test - void fileSystemResourceWithFilePath() throws Exception { - Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); - Resource resource = new FileSystemResource(filePath); - doTestResource(resource); - assertThat(resource).isEqualTo(new FileSystemResource(filePath)); - } - - @Test - void fileSystemResourceWithPlainPath() { - Resource resource = new FileSystemResource("core/io/Resource.class"); - assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); - } - - @Test - void urlResource() throws IOException { - Resource resource = new UrlResource(getClass().getResource("Resource.class")); - doTestResource(resource); - assertThat(resource).isEqualTo(new UrlResource(getClass().getResource("Resource.class"))); - - Resource resource2 = new UrlResource("file:core/io/Resource.class"); - assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource2); - assertThat(new UrlResource("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); - assertThat(new UrlResource("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); - assertThat(new UrlResource("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); - } - - private void doTestResource(Resource resource) throws IOException { + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void resourceIsValid(Resource resource) throws Exception { assertThat(resource.getFilename()).isEqualTo("Resource.class"); assertThat(resource.getURL().getFile().endsWith("Resource.class")).isTrue(); assertThat(resource.exists()).isTrue(); assertThat(resource.isReadable()).isTrue(); assertThat(resource.contentLength() > 0).isTrue(); assertThat(resource.lastModified() > 0).isTrue(); + } + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void resourceCreateRelative(Resource resource) throws Exception { Resource relative1 = resource.createRelative("ClassPathResource.class"); assertThat(relative1.getFilename()).isEqualTo("ClassPathResource.class"); assertThat(relative1.getURL().getFile().endsWith("ClassPathResource.class")).isTrue(); @@ -182,7 +82,11 @@ class ResourceTests { assertThat(relative1.isReadable()).isTrue(); assertThat(relative1.contentLength() > 0).isTrue(); assertThat(relative1.lastModified() > 0).isTrue(); + } + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void resourceCreateRelativeWithFolder(Resource resource) throws Exception { Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class"); assertThat(relative2.getFilename()).isEqualTo("ResourcePatternResolver.class"); assertThat(relative2.getURL().getFile().endsWith("ResourcePatternResolver.class")).isTrue(); @@ -190,7 +94,11 @@ class ResourceTests { assertThat(relative2.isReadable()).isTrue(); assertThat(relative2.contentLength() > 0).isTrue(); assertThat(relative2.lastModified() > 0).isTrue(); + } + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void resourceCreateRelativeWithDotPath(Resource resource) throws Exception { Resource relative3 = resource.createRelative("../SpringVersion.class"); assertThat(relative3.getFilename()).isEqualTo("SpringVersion.class"); assertThat(relative3.getURL().getFile().endsWith("SpringVersion.class")).isTrue(); @@ -198,7 +106,11 @@ class ResourceTests { assertThat(relative3.isReadable()).isTrue(); assertThat(relative3.contentLength() > 0).isTrue(); assertThat(relative3.lastModified() > 0).isTrue(); + } + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void resourceCreateRelativeUnknown(Resource resource) throws Exception { Resource relative4 = resource.createRelative("X.class"); assertThat(relative4.exists()).isFalse(); assertThat(relative4.isReadable()).isFalse(); @@ -208,126 +120,323 @@ class ResourceTests { relative4::lastModified); } - @Test - void classPathResourceWithRelativePath() throws IOException { - Resource resource = new ClassPathResource("dir/"); - Resource relative = resource.createRelative("subdir"); - assertThat(relative).isEqualTo(new ClassPathResource("dir/subdir")); + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void loadingMissingResourceFails(Resource resource) { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + resource.createRelative("X").getInputStream()); } - @Test - void fileSystemResourceWithRelativePath() throws IOException { - Resource resource = new FileSystemResource("dir/"); - Resource relative = resource.createRelative("subdir"); - assertThat(relative).isEqualTo(new FileSystemResource("dir/subdir")); + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("resource") + void readingMissingResourceFails(Resource resource) { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + resource.createRelative("X").readableChannel()); } - @Test - void urlResourceWithRelativePath() throws IOException { - Resource resource = new UrlResource("file:dir/"); - Resource relative = resource.createRelative("subdir"); - assertThat(relative).isEqualTo(new UrlResource("file:dir/subdir")); + private static Stream resource() throws URISyntaxException { + URL resourceClass = ResourceTests.class.getResource("Resource.class"); + Path resourceClassFilePath = Paths.get(resourceClass.toURI()); + return Stream.of( + Arguments.of(Named.of("ClassPathResource", new ClassPathResource("org/springframework/core/io/Resource.class"))), + Arguments.of(Named.of("ClassPathResource with ClassLoader", new ClassPathResource("org/springframework/core/io/Resource.class", ResourceTests.class.getClassLoader()))), + Arguments.of(Named.of("ClassPathResource with Class", new ClassPathResource("Resource.class", ResourceTests.class))), + Arguments.of(Named.of("FileSystemResource", new FileSystemResource(resourceClass.getFile()))), + Arguments.of(Named.of("FileSystemResource with File", new FileSystemResource(new File(resourceClass.getFile())))), + Arguments.of(Named.of("FileSystemResource with File path", new FileSystemResource(resourceClassFilePath))), + Arguments.of(Named.of("UrlResource", new UrlResource(resourceClass))) + ); } - @Test - void nonFileResourceExists() throws Exception { - URL url = new URL("https://spring.io/"); - // Abort if spring.io is not reachable. - assumeTrue(urlIsReachable(url)); + @Nested + class ByteArrayResourceTests { + + @Test + void hasContent() throws Exception { + Resource resource = new ByteArrayResource("testString".getBytes()); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isFalse(); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(new ByteArrayResource("testString".getBytes())).isEqualTo(resource); + } + + @Test + void isNotOpen() { + Resource resource = new ByteArrayResource("testString".getBytes()); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isFalse(); + } + + @Test + void hasDescription() { + Resource resource = new ByteArrayResource("testString".getBytes(), "my description"); + assertThat(resource.getDescription().contains("my description")).isTrue(); + } - Resource resource = new UrlResource(url); - assertThat(resource.exists()).isTrue(); } - private boolean urlIsReachable(URL url) { - try { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("HEAD"); - connection.setReadTimeout(5_000); - return connection.getResponseCode() == HttpURLConnection.HTTP_OK; + @Nested + class InputStreamResourceTests { + + @Test + void hasContent() throws Exception { + InputStream is = new ByteArrayInputStream("testString".getBytes()); + Resource resource = new InputStreamResource(is); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(new InputStreamResource(is)).isEqualTo(resource); } - catch (Exception ex) { - return false; + + @Test + void isOpen() { + InputStream is = new ByteArrayInputStream("testString".getBytes()); + Resource resource = new InputStreamResource(is); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isTrue(); + } + + @Test + void hasDescription() { + InputStream is = new ByteArrayInputStream("testString".getBytes()); + Resource resource = new InputStreamResource(is, "my description"); + assertThat(resource.getDescription().contains("my description")).isTrue(); } } - @Test - void abstractResourceExceptions() throws Exception { - final String name = "test-resource"; - Resource resource = new AbstractResource() { - @Override - public String getDescription() { - return name; - } - @Override - public InputStream getInputStream() throws IOException { - throw new FileNotFoundException(); - } - }; + @Nested + class ClassPathResourceTests { + + @Test + void equalsAndHashCode() { + Resource resource = new ClassPathResource("org/springframework/core/io/Resource.class"); + Resource resource2 = new ClassPathResource("org/springframework/core/../core/io/./Resource.class"); + Resource resource3 = new ClassPathResource("org/springframework/core/").createRelative("../core/io/./Resource.class"); + assertThat(resource2).isEqualTo(resource); + assertThat(resource3).isEqualTo(resource); + // Check whether equal/hashCode works in a HashSet. + HashSet resources = new HashSet<>(); + resources.add(resource); + resources.add(resource2); + assertThat(resources.size()).isEqualTo(1); + } - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( - resource::getURL) - .withMessageContaining(name); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( - resource::getFile) - .withMessageContaining(name); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - resource.createRelative("/testing")) - .withMessageContaining(name); + @Test + void resourcesWithDifferentPathsAreEqual() { + Resource resource = new ClassPathResource("org/springframework/core/io/Resource.class", getClass().getClassLoader()); + ClassPathResource sameResource = new ClassPathResource("org/springframework/core/../core/io/./Resource.class", getClass().getClassLoader()); + assertThat(sameResource).isEqualTo(resource); + } + + @Test + void relativeResourcesAreEqual() throws Exception { + Resource resource = new ClassPathResource("dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new ClassPathResource("dir/subdir")); + } - assertThat(resource.getFilename()).isNull(); } - @Test - void contentLength() throws IOException { - AbstractResource resource = new AbstractResource() { - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(new byte[] { 'a', 'b', 'c' }); - } - @Override - public String getDescription() { - return ""; + @Nested + class FileSystemResourceTests { + + @Test + void sameResourceIsEqual() { + String file = getClass().getResource("Resource.class").getFile(); + Resource resource = new FileSystemResource(file); + assertThat(resource).isEqualTo(new FileSystemResource(file)); + } + + @Test + void sameResourceFromFileIsEqual() { + File file = new File(getClass().getResource("Resource.class").getFile()); + Resource resource = new FileSystemResource(file); + assertThat(resource).isEqualTo(new FileSystemResource(file)); + } + + @Test + void sameResourceFromFilePathIsEqual() throws Exception { + Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); + Resource resource = new FileSystemResource(filePath); + assertThat(resource).isEqualTo(new FileSystemResource(filePath)); + } + + @Test + void sameResourceFromDotPathIsEqual() { + Resource resource = new FileSystemResource("core/io/Resource.class"); + assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); + } + + @Test + void relativeResourcesAreEqual() throws Exception { + Resource resource = new FileSystemResource("dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new FileSystemResource("dir/subdir")); + } + + @Test + void readableChannelProvidesContent() throws Exception { + Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile()); + try (ReadableByteChannel channel = resource.readableChannel()) { + ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); + channel.read(buffer); + buffer.rewind(); + assertThat(buffer.limit() > 0).isTrue(); } - }; - assertThat(resource.contentLength()).isEqualTo(3L); + } + } - @Test - void readableChannel() throws IOException { - Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile()); - try (ReadableByteChannel channel = resource.readableChannel()) { - ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); - channel.read(buffer); - buffer.rewind(); - assertThat(buffer.limit() > 0).isTrue(); + @Nested + class UrlResourceTests { + + private MockWebServer server = new MockWebServer(); + + @Test + void sameResourceWithRelativePathIsEqual() throws Exception { + Resource resource = new UrlResource("file:core/io/Resource.class"); + assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource); } - } - @Test - void inputStreamNotFoundOnFileSystemResource() throws IOException { - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - new FileSystemResource(getClass().getResource("Resource.class").getFile()).createRelative("X").getInputStream()); - } + @Test + void filenameIsExtractedFromFilePath() throws Exception { + assertThat(new UrlResource("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); + } - @Test - void readableChannelNotFoundOnFileSystemResource() throws IOException { - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - new FileSystemResource(getClass().getResource("Resource.class").getFile()).createRelative("X").readableChannel()); - } + @Test + void relativeResourcesAreEqual() throws Exception { + Resource resource = new UrlResource("file:dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new UrlResource("file:dir/subdir")); + } - @Test - void inputStreamNotFoundOnClassPathResource() throws IOException { - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - new ClassPathResource("Resource.class", getClass()).createRelative("X").getInputStream()); + @Test + void missingRemoteResourceDoesNotExist() throws Exception { + String baseUrl = startServer(); + UrlResource resource = new UrlResource(baseUrl + "/missing"); + assertThat(resource.exists()).isFalse(); + } + + @Test + void remoteResourceExists() throws Exception { + String baseUrl = startServer(); + UrlResource resource = new UrlResource(baseUrl + "/resource"); + assertThat(resource.exists()).isTrue(); + assertThat(resource.contentLength()).isEqualTo(6); + } + + @Test + void canCustomizeHttpUrlConnectionForExists() throws Exception { + String baseUrl = startServer(); + CustomResource resource = new CustomResource(baseUrl + "/resource"); + assertThat(resource.exists()).isTrue(); + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("HEAD"); + assertThat(request.getHeader("Framework-Name")).isEqualTo("Spring"); + } + + @Test + void canCustomizeHttpUrlConnectionForRead() throws Exception { + String baseUrl = startServer(); + CustomResource resource = new CustomResource(baseUrl + "/resource"); + assertThat(resource.getInputStream()).hasContent("Spring"); + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getHeader("Framework-Name")).isEqualTo("Spring"); + } + + @AfterEach + void shutdown() throws Exception { + this.server.shutdown(); + } + + private String startServer() throws Exception { + this.server.setDispatcher(new ResourceDispatcher()); + this.server.start(); + return "http://localhost:" + this.server.getPort(); + } + + class CustomResource extends UrlResource { + + public CustomResource(String path) throws MalformedURLException { + super(path); + } + + @Override + protected void customizeConnection(HttpURLConnection con) throws IOException { + con.setRequestProperty("Framework-Name", "Spring"); + } + } + + class ResourceDispatcher extends Dispatcher { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().equals("/resource")) { + switch (request.getMethod()) { + case "HEAD": + return new MockResponse() + .addHeader("Content-Length", "6"); + case "GET": + return new MockResponse() + .addHeader("Content-Length", "6") + .addHeader("Content-Type", "text/plain") + .setBody("Spring"); + } + } + return new MockResponse().setResponseCode(404); + } + } } - @Test - void readableChannelNotFoundOnClassPathResource() throws IOException { - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> - new ClassPathResource("Resource.class", getClass()).createRelative("X").readableChannel()); + @Nested + class AbstractResourceTests { + + @Test + void missingResourceIsNotReadable() { + final String name = "test-resource"; + + Resource resource = new AbstractResource() { + @Override + public String getDescription() { + return name; + } + + @Override + public InputStream getInputStream() throws IOException { + throw new FileNotFoundException(); + } + }; + + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getURL) + .withMessageContaining(name); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getFile) + .withMessageContaining(name); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + resource.createRelative("/testing")).withMessageContaining(name); + assertThat(resource.getFilename()).isNull(); + } + + @Test + void hasContentLength() throws Exception { + AbstractResource resource = new AbstractResource() { + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); + } + + @Override + public String getDescription() { + return ""; + } + }; + assertThat(resource.contentLength()).isEqualTo(3L); + } + } }