diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java
index 7b53e70ff3a..de26e7b7067 100644
--- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java
+++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java
@@ -108,7 +108,7 @@ public class PathEditor extends PropertyEditorSupport {
}
else {
try {
- setValue(resource.getFile().toPath());
+ setValue(resource.getFilePath());
}
catch (IOException ex) {
String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " +
diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java
index cae57ac8ce4..ec51f4cefd3 100644
--- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java
+++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java
@@ -292,6 +292,14 @@ public class FileSystemResource extends AbstractResource implements WritableReso
return (this.file != null ? this.file : this.filePath.toFile());
}
+ /**
+ * This implementation returns the underlying NIO Path reference.
+ */
+ @Override
+ public Path getFilePath() {
+ return this.filePath;
+ }
+
/**
* This implementation opens a FileChannel for the underlying file.
* @see java.nio.channels.FileChannel
diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java
index 7a6bd448823..9b8eaaefe83 100644
--- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java
+++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java
@@ -219,15 +219,16 @@ public class PathResource extends AbstractResource implements WritableResource {
* This implementation returns the underlying {@link File} reference.
*/
@Override
- public File getFile() throws IOException {
- try {
- return this.path.toFile();
- }
- catch (UnsupportedOperationException ex) {
- // Only paths on the default file system can be converted to a File:
- // Do exception translation for cases where conversion is not possible.
- throw new FileNotFoundException(this.path + " cannot be resolved to absolute file path");
- }
+ public File getFile() {
+ return this.path.toFile();
+ }
+
+ /**
+ * This implementation returns the underlying {@link Path} reference.
+ */
+ @Override
+ public Path getFilePath() {
+ return this.path;
}
/**
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 0ae3750f8d5..b7e39449407 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
@@ -25,6 +25,7 @@ import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import org.jspecify.annotations.Nullable;
@@ -91,11 +92,13 @@ public interface Resource extends InputStreamSource {
/**
* Determine whether this resource represents a file in a file system.
- *
A value of {@code true} strongly suggests (but does not guarantee)
- * that a {@link #getFile()} call will succeed.
+ *
A value of {@code true} suggests (but does not guarantee) that a
+ * {@link #getFile()} call will succeed. For non-default file systems,
+ * {@link #getFilePath()} is the more reliable follow-up call.
*
This is conservatively {@code false} by default.
* @since 5.0
* @see #getFile()
+ * @see #getFilePath()
*/
default boolean isFile() {
return false;
@@ -118,13 +121,26 @@ public interface Resource extends InputStreamSource {
/**
* Return a File handle for this resource.
- * @throws java.io.FileNotFoundException if the resource cannot be resolved as
- * absolute file path, i.e. if the resource is not available in a file system
+ *
Note: This only works for files in the default file system.
+ * @throws UnsupportedOperationException if the resource is a file but cannot be
+ * exposed as a {@code java.io.File}; try {@link #getFilePath()} instead
+ * @throws java.io.FileNotFoundException if the resource cannot be resolved as a file
* @throws IOException in case of general resolution/reading failures
* @see #getInputStream()
*/
File getFile() throws IOException;
+ /**
+ * Return an NIO Path handle for this resource.
+ *
Note: This works for files in non-default file systems as well.
+ * @throws java.io.FileNotFoundException if the resource cannot be resolved as a file
+ * @throws IOException in case of general resolution/reading failures
+ * @since 7.0
+ */
+ default Path getFilePath() throws IOException {
+ return getFile().toPath();
+ }
+
/**
* Return a {@link ReadableByteChannel}.
*
It is expected that each call creates a fresh channel.
diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
index 4c3c430e954..5875b3c0d6a 100644
--- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
+++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
@@ -16,7 +16,6 @@
package org.springframework.core.io.buffer;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -223,9 +222,9 @@ public abstract class DataBufferUtils {
try {
if (resource.isFile()) {
- File file = resource.getFile();
+ Path filePath = resource.getFilePath();
return readAsynchronousFileChannel(
- () -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ),
+ () -> AsynchronousFileChannel.open(filePath, StandardOpenOption.READ),
position, bufferFactory, bufferSize);
}
}
@@ -233,7 +232,7 @@ public abstract class DataBufferUtils {
// fallback to resource.readableChannel(), below
}
Flux result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize);
- return position == 0 ? result : skipUntilByteCount(result, position);
+ return (position == 0 ? result : skipUntilByteCount(result, position));
}
diff --git a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java
index af9daeee0cb..5823195032f 100644
--- a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java
+++ b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java
@@ -186,10 +186,11 @@ class PathResourceTests {
}
@Test
- void getFile() throws IOException {
+ void getFile() {
PathResource resource = new PathResource(TEST_FILE);
File file = new File(TEST_FILE);
assertThat(resource.getFile().getAbsoluteFile()).isEqualTo(file.getAbsoluteFile());
+ assertThat(resource.getFilePath()).isEqualTo(file.toPath());
}
@Test
@@ -198,7 +199,7 @@ class PathResourceTests {
given(path.normalize()).willReturn(path);
given(path.toFile()).willThrow(new UnsupportedOperationException());
PathResource resource = new PathResource(path);
- assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(resource::getFile);
+ assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(resource::getFile);
}
@Test
@@ -236,13 +237,13 @@ class PathResourceTests {
@Test
void filename() {
- Resource resource = new PathResource(TEST_FILE);
+ PathResource resource = new PathResource(TEST_FILE);
assertThat(resource.getFilename()).isEqualTo("example.properties");
}
@Test
void description() {
- Resource resource = new PathResource(TEST_FILE);
+ PathResource resource = new PathResource(TEST_FILE);
assertThat(resource.getDescription()).contains("path [");
assertThat(resource.getDescription()).contains(TEST_FILE);
}
@@ -261,9 +262,9 @@ class PathResourceTests {
@Test
void equalsAndHashCode() {
- Resource resource1 = new PathResource(TEST_FILE);
- Resource resource2 = new PathResource(TEST_FILE);
- Resource resource3 = new PathResource(TEST_DIR);
+ PathResource resource1 = new PathResource(TEST_FILE);
+ PathResource resource2 = new PathResource(TEST_FILE);
+ PathResource resource3 = new PathResource(TEST_DIR);
assertThat(resource1).isEqualTo(resource1);
assertThat(resource1).isEqualTo(resource2);
assertThat(resource2).isEqualTo(resource1);
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 ffb08bc50c3..9385a210f99 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
@@ -55,6 +55,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
/**
* Tests for various {@link Resource} implementations.
@@ -268,6 +270,16 @@ class ResourceTests {
assertThat(relative).isEqualTo(new FileSystemResource("dir/subdir"));
}
+ @Test
+ void getFilePath() throws Exception {
+ Path path = mock();
+ given(path.normalize()).willReturn(path);
+ given(path.toFile()).willThrow(new UnsupportedOperationException());
+ Resource resource = new FileSystemResource(path);
+ assertThat(resource.getFilePath()).isSameAs(path);
+ assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(resource::getFile);
+ }
+
@Test
void readableChannelProvidesContent() throws Exception {
Resource resource = new FileSystemResource(getClass().getResource("ResourceTests.class").getFile());
diff --git a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java
index a464a12108f..5e56fe1e482 100644
--- a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java
+++ b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java
@@ -16,8 +16,9 @@
package org.springframework.http.codec;
-import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.List;
import java.util.Map;
@@ -193,17 +194,17 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter {
if (message instanceof ZeroCopyHttpOutputMessage zeroCopyHttpOutputMessage && resource.isFile()) {
try {
- File file = resource.getFile();
- long pos = region != null ? region.getPosition() : 0;
- long count = region != null ? region.getCount() : file.length();
+ Path filePath = resource.getFilePath();
+ long pos = (region != null ? region.getPosition() : 0);
+ long count = (region != null ? region.getCount() : Files.size(filePath));
if (logger.isDebugEnabled()) {
String formatted = region != null ? "region " + pos + "-" + (count) + " of " : "";
logger.debug(Hints.getLogPrefix(hints) + "Zero-copy " + formatted + "[" + resource + "]");
}
- return zeroCopyHttpOutputMessage.writeWith(file, pos, count);
+ return zeroCopyHttpOutputMessage.writeWith(filePath, pos, count);
}
- catch (IOException ex) {
- // should not happen
+ catch (IOException ignore) {
+ // returning null below leads to fallback code path
}
}
return null;
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java
index 2b12943e3a2..07c9345695b 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/EncodedResourceResolver.java
@@ -23,6 +23,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -240,6 +241,11 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return this.encoded.getFile();
}
+ @Override
+ public Path getFilePath() throws IOException {
+ return this.encoded.getFilePath();
+ }
+
@Override
public InputStream getInputStream() throws IOException {
return this.encoded.getInputStream();
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java
index 24f2f34efb6..860d055a0a0 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java
@@ -23,6 +23,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -283,6 +284,11 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return this.original.getFile();
}
+ @Override
+ public Path getFilePath() throws IOException {
+ return this.original.getFilePath();
+ }
+
@Override
public InputStream getInputStream() throws IOException {
return this.original.getInputStream();
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java
index 24779113504..0b80b3b40d8 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResourceResolver.java
@@ -23,6 +23,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -236,6 +237,11 @@ public class EncodedResourceResolver extends AbstractResourceResolver {
return this.encoded.getFile();
}
+ @Override
+ public Path getFilePath() throws IOException {
+ return this.encoded.getFilePath();
+ }
+
@Override
public InputStream getInputStream() throws IOException {
return this.encoded.getInputStream();
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
index d521889f2f0..9786ffe9954 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
@@ -23,6 +23,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -279,6 +280,11 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return this.original.getFile();
}
+ @Override
+ public Path getFilePath() throws IOException {
+ return this.original.getFilePath();
+ }
+
@Override
public InputStream getInputStream() throws IOException {
return this.original.getInputStream();