Expose getFilePath() on Resource interface for consistent NIO support

Closes gh-35435
This commit is contained in:
Juergen Hoeller
2025-09-08 17:03:07 +02:00
parent 114c3f7c9c
commit 27221581a1
12 changed files with 94 additions and 32 deletions
@@ -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 + ": " +
@@ -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
@@ -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;
}
/**
@@ -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.
* <p>A value of {@code true} strongly suggests (but does not guarantee)
* that a {@link #getFile()} call will succeed.
* <p>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.
* <p>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
* <p>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.
* <p>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}.
* <p>It is expected that each call creates a <i>fresh</i> channel.
@@ -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<DataBuffer> result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize);
return position == 0 ? result : skipUntilByteCount(result, position);
return (position == 0 ? result : skipUntilByteCount(result, position));
}
@@ -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);
@@ -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());
@@ -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<Resource> {
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;
@@ -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();
@@ -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();
@@ -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();
@@ -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();