diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index 1070232d48..740d571931 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -158,10 +158,11 @@ public interface Resource extends InputStreamSource { Resource createRelative(String relativePath) throws IOException; /** - * Determine a filename for this resource, i.e. typically the last - * part of the path: for example, "myfile.txt". + * Determine the filename for this resource — typically the last + * part of the path — for example, {@code "myfile.txt"}. *
Returns {@code null} if this type of resource does not * have a filename. + *
Implementations are encouraged to return the filename unencoded. */ @Nullable String getFilename(); 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 bcb559652f..72e43ae4ef 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 @@ -26,6 +26,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -316,12 +318,15 @@ public class UrlResource extends AbstractFileResolvingResource { } /** - * This implementation returns the name of the file that this URL refers to. + * This implementation returns the URL-decoded name of the file that this URL + * refers to. * @see java.net.URL#getPath() + * @see java.net.URLDecoder#decode(String, java.nio.charset.Charset) */ @Override public String getFilename() { - return StringUtils.getFilename(getCleanedUrl().getPath()); + String filename = StringUtils.getFilename(getCleanedUrl().getPath()); + return URLDecoder.decode(filename, StandardCharsets.UTF_8); } /** 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 b0ce104fef..7549db9a63 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 @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; @@ -59,7 +60,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; */ class ResourceTests { - @ParameterizedTest(name = "{index}: {0}") @MethodSource("resource") void resourceIsValid(Resource resource) throws Exception { @@ -272,6 +272,22 @@ class ResourceTests { assertThat(new UrlResource("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); } + @Test + void filenameContainingHashTagIsExtractedFromFilePathUnencoded() throws Exception { + String unencodedPath = "/dir/test#1.txt"; + String encodedPath = "/dir/test%231.txt"; + + URI uri = new URI("file", unencodedPath, null); + URL url = uri.toURL(); + assertThat(uri.getPath()).isEqualTo(unencodedPath); + assertThat(uri.getRawPath()).isEqualTo(encodedPath); + assertThat(url.getPath()).isEqualTo(encodedPath); + + UrlResource urlResource = new UrlResource(url); + assertThat(urlResource.getURI().getPath()).isEqualTo(unencodedPath); + assertThat(urlResource.getFilename()).isEqualTo("test#1.txt"); + } + @Test void factoryMethodsProduceEqualResources() throws Exception { Resource resource1 = new UrlResource("file:core/io/Resource.class");