Resource.lastModified() propagates 0 value if target resource exists
Includes use of Files.getLastModifiedTime for NIO Paths, preservation of NIO-based resolution on createRelative, deprecation of PathResource, and consistent use of getContentLengthLong over getContentLength. Issue: SPR-17320
This commit is contained in:
parent
1e0de072f9
commit
cf3635b42d
|
|
@ -65,7 +65,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (con.getContentLength() >= 0) {
|
if (con.getContentLengthLong() > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (httpCon != null) {
|
if (httpCon != null) {
|
||||||
|
|
@ -106,7 +106,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int contentLength = con.getContentLength();
|
long contentLength = con.getContentLengthLong();
|
||||||
if (contentLength > 0) {
|
if (contentLength > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -226,23 +226,35 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
||||||
URL url = getURL();
|
URL url = getURL();
|
||||||
if (ResourceUtils.isFileURL(url)) {
|
if (ResourceUtils.isFileURL(url)) {
|
||||||
// Proceed with file system resolution
|
// Proceed with file system resolution
|
||||||
return getFile().length();
|
File file = getFile();
|
||||||
|
long length = file.length();
|
||||||
|
if (length == 0L && !file.exists()) {
|
||||||
|
throw new FileNotFoundException(getDescription() +
|
||||||
|
" cannot be resolved in the file system for checking its content length");
|
||||||
|
}
|
||||||
|
return length;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Try a URL connection content-length header
|
// Try a URL connection content-length header
|
||||||
URLConnection con = url.openConnection();
|
URLConnection con = url.openConnection();
|
||||||
customizeConnection(con);
|
customizeConnection(con);
|
||||||
return con.getContentLength();
|
return con.getContentLengthLong();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long lastModified() throws IOException {
|
public long lastModified() throws IOException {
|
||||||
URL url = getURL();
|
URL url = getURL();
|
||||||
|
boolean fileCheck = false;
|
||||||
if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) {
|
if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) {
|
||||||
// Proceed with file system resolution
|
// Proceed with file system resolution
|
||||||
|
fileCheck = true;
|
||||||
try {
|
try {
|
||||||
return super.lastModified();
|
File fileToCheck = getFileForLastModifiedCheck();
|
||||||
|
long lastModified = fileToCheck.lastModified();
|
||||||
|
if (lastModified > 0L || fileToCheck.exists()) {
|
||||||
|
return lastModified;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException ex) {
|
catch (FileNotFoundException ex) {
|
||||||
// Defensively fall back to URL connection check instead
|
// Defensively fall back to URL connection check instead
|
||||||
|
|
@ -251,7 +263,12 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
||||||
// Try a URL connection last-modified header
|
// Try a URL connection last-modified header
|
||||||
URLConnection con = url.openConnection();
|
URLConnection con = url.openConnection();
|
||||||
customizeConnection(con);
|
customizeConnection(con);
|
||||||
return con.getLastModified();
|
long lastModified = con.getLastModified();
|
||||||
|
if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) {
|
||||||
|
throw new FileNotFoundException(getDescription() +
|
||||||
|
" cannot be resolved in the file system for checking its last-modified timestamp");
|
||||||
|
}
|
||||||
|
return lastModified;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ public abstract class AbstractResource implements Resource {
|
||||||
InputStream is = getInputStream();
|
InputStream is = getInputStream();
|
||||||
try {
|
try {
|
||||||
long size = 0;
|
long size = 0;
|
||||||
byte[] buf = new byte[255];
|
byte[] buf = new byte[256];
|
||||||
int read;
|
int read;
|
||||||
while ((read = is.read(buf)) != -1) {
|
while ((read = is.read(buf)) != -1) {
|
||||||
size += read;
|
size += read;
|
||||||
|
|
@ -169,10 +169,11 @@ public abstract class AbstractResource implements Resource {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public long lastModified() throws IOException {
|
public long lastModified() throws IOException {
|
||||||
long lastModified = getFileForLastModifiedCheck().lastModified();
|
File fileToCheck = getFileForLastModifiedCheck();
|
||||||
if (lastModified == 0L) {
|
long lastModified = fileToCheck.lastModified();
|
||||||
|
if (lastModified == 0L && !fileToCheck.exists()) {
|
||||||
throw new FileNotFoundException(getDescription() +
|
throw new FileNotFoundException(getDescription() +
|
||||||
" cannot be resolved in the file system for resolving its last-modified timestamp");
|
" cannot be resolved in the file system for checking its last-modified timestamp");
|
||||||
}
|
}
|
||||||
return lastModified;
|
return lastModified;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import java.net.URL;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.ReadableByteChannel;
|
import java.nio.channels.ReadableByteChannel;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
@ -86,10 +87,10 @@ public class FileSystemResource extends AbstractResource implements WritableReso
|
||||||
* <p>Note: When building relative resources via {@link #createRelative},
|
* <p>Note: When building relative resources via {@link #createRelative},
|
||||||
* the relative path will apply <i>at the same directory level</i>:
|
* the relative path will apply <i>at the same directory level</i>:
|
||||||
* e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"!
|
* e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"!
|
||||||
* If you prefer to have relative paths built underneath the given root
|
* If you prefer to have relative paths built underneath the given root directory,
|
||||||
* directory, use the {@link #FileSystemResource(String) constructor with a file path}
|
* use the {@link #FileSystemResource(String) constructor with a file path}
|
||||||
* to append a trailing slash to the root path: "C:/dir1/", which
|
* to append a trailing slash to the root path: "C:/dir1/", which indicates
|
||||||
* indicates this directory as root for all relative paths.
|
* this directory as root for all relative paths.
|
||||||
* @param file a File handle
|
* @param file a File handle
|
||||||
* @see #FileSystemResource(Path)
|
* @see #FileSystemResource(Path)
|
||||||
* @see #getFile()
|
* @see #getFile()
|
||||||
|
|
@ -102,20 +103,38 @@ public class FileSystemResource extends AbstractResource implements WritableReso
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@code FileSystemResource} from a {@link Path} handle.
|
* Create a new {@code FileSystemResource} from a {@link Path} handle,
|
||||||
|
* performing all file system interactions via NIO.2 instead of {@link File}.
|
||||||
* <p>In contrast to {@link PathResource}, this variant strictly follows the
|
* <p>In contrast to {@link PathResource}, this variant strictly follows the
|
||||||
* general {@link FileSystemResource} conventions, in particular in terms of
|
* general {@link FileSystemResource} conventions, in particular in terms of
|
||||||
* path cleaning and {@link #createRelative(String)} handling.
|
* path cleaning and {@link #createRelative(String)} handling.
|
||||||
* @param filePath a Path handle to a file
|
* @param filePath a Path handle to a file
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
* @see #FileSystemResource(File)
|
* @see #FileSystemResource(File)
|
||||||
* @see PathResource
|
|
||||||
*/
|
*/
|
||||||
public FileSystemResource(Path filePath) {
|
public FileSystemResource(Path filePath) {
|
||||||
Assert.notNull(filePath, "Path must not be null");
|
Assert.notNull(filePath, "Path must not be null");
|
||||||
this.filePath = filePath;
|
|
||||||
this.file = null;
|
|
||||||
this.path = StringUtils.cleanPath(filePath.toString());
|
this.path = StringUtils.cleanPath(filePath.toString());
|
||||||
|
this.file = null;
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code FileSystemResource} from a {@link FileSystem} handle,
|
||||||
|
* locating the specified path.
|
||||||
|
* <p>This is an alternative to {@link #FileSystemResource(String)},
|
||||||
|
* performing all file system interactions via NIO.2 instead of {@link File}.
|
||||||
|
* @param fileSystem the FileSystem to locate the path within
|
||||||
|
* @param path a file path
|
||||||
|
* @since 5.1.1
|
||||||
|
* @see #FileSystemResource(File)
|
||||||
|
*/
|
||||||
|
public FileSystemResource(FileSystem fileSystem, String path) {
|
||||||
|
Assert.notNull(fileSystem, "FileSystem must not be null");
|
||||||
|
Assert.notNull(path, "Path must not be null");
|
||||||
|
this.path = StringUtils.cleanPath(path);
|
||||||
|
this.file = null;
|
||||||
|
this.filePath = fileSystem.getPath(this.path).normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,11 +259,44 @@ public class FileSystemResource extends AbstractResource implements WritableReso
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This implementation returns the underlying File's length.
|
* This implementation returns the underlying File/Path length.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public long contentLength() throws IOException {
|
public long contentLength() throws IOException {
|
||||||
return (this.file != null ? this.file.length() : Files.size(this.filePath));
|
if (this.file != null) {
|
||||||
|
long length = this.file.length();
|
||||||
|
if (length == 0L && !this.file.exists()) {
|
||||||
|
throw new FileNotFoundException(getDescription() +
|
||||||
|
" cannot be resolved in the file system for checking its content length");
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
return Files.size(this.filePath);
|
||||||
|
}
|
||||||
|
catch (NoSuchFileException ex) {
|
||||||
|
throw new FileNotFoundException(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implementation returns the underlying File/Path last-modified time.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long lastModified() throws IOException {
|
||||||
|
if (this.file != null) {
|
||||||
|
return super.lastModified();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
return Files.getLastModifiedTime(this.filePath).toMillis();
|
||||||
|
}
|
||||||
|
catch (NoSuchFileException ex) {
|
||||||
|
throw new FileNotFoundException(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -255,7 +307,8 @@ public class FileSystemResource extends AbstractResource implements WritableReso
|
||||||
@Override
|
@Override
|
||||||
public Resource createRelative(String relativePath) {
|
public Resource createRelative(String relativePath) {
|
||||||
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
|
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
|
||||||
return new FileSystemResource(pathToUse);
|
return (this.file != null ? new FileSystemResource(pathToUse) :
|
||||||
|
new FileSystemResource(this.filePath.getFileSystem(), pathToUse));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,8 @@ import org.springframework.util.ResourceUtils;
|
||||||
* <p>This is the class resolved by {@link DefaultResourceLoader} for a "file:..."
|
* <p>This is the class resolved by {@link DefaultResourceLoader} for a "file:..."
|
||||||
* URL location, allowing a downcast to {@link WritableResource} for it.
|
* URL location, allowing a downcast to {@link WritableResource} for it.
|
||||||
*
|
*
|
||||||
* <p>Alternatively, for direct construction from a {@link java.io.File} handle,
|
* <p>Alternatively, for direct construction from a {@link java.io.File} handle
|
||||||
* consider using {@link FileSystemResource}. For an NIO {@link java.nio.file.Path},
|
* or NIO {@link java.nio.file.Path}, consider using {@link FileSystemResource}.
|
||||||
* consider using {@link PathResource} instead.
|
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @since 5.0.2
|
* @since 5.0.2
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,11 @@ import org.springframework.util.Assert;
|
||||||
* @author Philippe Marschall
|
* @author Philippe Marschall
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @since 4.0
|
* @since 4.0
|
||||||
* @see FileSystemResource#FileSystemResource(Path)
|
|
||||||
* @see java.nio.file.Path
|
* @see java.nio.file.Path
|
||||||
* @see java.nio.file.Files
|
* @see java.nio.file.Files
|
||||||
|
* @deprecated as of 5.1.1, in favor of {@link FileSystemResource#FileSystemResource(Path)}
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public class PathResource extends AbstractResource implements WritableResource {
|
public class PathResource extends AbstractResource implements WritableResource {
|
||||||
|
|
||||||
private final Path path;
|
private final Path path;
|
||||||
|
|
@ -105,7 +106,7 @@ public class PathResource extends AbstractResource implements WritableResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This implementation returns whether the underlying file exists.
|
* This implementation returns whether the underlying file exists.
|
||||||
* @see org.springframework.core.io.PathResource#exists()
|
* @see java.nio.file.Files#exists(Path, java.nio.file.LinkOption...)
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean exists() {
|
public boolean exists() {
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@ import org.springframework.lang.Nullable;
|
||||||
* @see WritableResource
|
* @see WritableResource
|
||||||
* @see ContextResource
|
* @see ContextResource
|
||||||
* @see UrlResource
|
* @see UrlResource
|
||||||
* @see ClassPathResource
|
* @see FileUrlResource
|
||||||
* @see FileSystemResource
|
* @see FileSystemResource
|
||||||
* @see PathResource
|
* @see ClassPathResource
|
||||||
* @see ByteArrayResource
|
* @see ByteArrayResource
|
||||||
* @see InputStreamResource
|
* @see InputStreamResource
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -123,11 +123,10 @@ public class ResourceTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFileSystemResource() throws IOException {
|
public void testFileSystemResource() throws IOException {
|
||||||
Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile());
|
String file = getClass().getResource("Resource.class").getFile();
|
||||||
|
Resource resource = new FileSystemResource(file);
|
||||||
doTestResource(resource);
|
doTestResource(resource);
|
||||||
assertEquals(new FileSystemResource(getClass().getResource("Resource.class").getFile()), resource);
|
assertEquals(new FileSystemResource(file), resource);
|
||||||
Resource resource2 = new FileSystemResource("core/io/Resource.class");
|
|
||||||
assertEquals(resource2, new FileSystemResource("core/../core/io/./Resource.class"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -136,8 +135,12 @@ public class ResourceTests {
|
||||||
Resource resource = new FileSystemResource(filePath);
|
Resource resource = new FileSystemResource(filePath);
|
||||||
doTestResource(resource);
|
doTestResource(resource);
|
||||||
assertEquals(new FileSystemResource(filePath), resource);
|
assertEquals(new FileSystemResource(filePath), resource);
|
||||||
Resource resource2 = new FileSystemResource("core/io/Resource.class");
|
}
|
||||||
assertEquals(resource2, new FileSystemResource("core/../core/io/./Resource.class"));
|
|
||||||
|
@Test
|
||||||
|
public void testFileSystemResourceWithPlainPath() {
|
||||||
|
Resource resource = new FileSystemResource("core/io/Resource.class");
|
||||||
|
assertEquals(resource, new FileSystemResource("core/../core/io/./Resource.class"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -157,23 +160,52 @@ public class ResourceTests {
|
||||||
private void doTestResource(Resource resource) throws IOException {
|
private void doTestResource(Resource resource) throws IOException {
|
||||||
assertEquals("Resource.class", resource.getFilename());
|
assertEquals("Resource.class", resource.getFilename());
|
||||||
assertTrue(resource.getURL().getFile().endsWith("Resource.class"));
|
assertTrue(resource.getURL().getFile().endsWith("Resource.class"));
|
||||||
|
assertTrue(resource.exists());
|
||||||
|
assertTrue(resource.isReadable());
|
||||||
|
assertTrue(resource.contentLength() > 0);
|
||||||
|
assertTrue(resource.lastModified() > 0);
|
||||||
|
|
||||||
Resource relative1 = resource.createRelative("ClassPathResource.class");
|
Resource relative1 = resource.createRelative("ClassPathResource.class");
|
||||||
assertEquals("ClassPathResource.class", relative1.getFilename());
|
assertEquals("ClassPathResource.class", relative1.getFilename());
|
||||||
assertTrue(relative1.getURL().getFile().endsWith("ClassPathResource.class"));
|
assertTrue(relative1.getURL().getFile().endsWith("ClassPathResource.class"));
|
||||||
assertTrue(relative1.exists());
|
assertTrue(relative1.exists());
|
||||||
|
assertTrue(relative1.isReadable());
|
||||||
|
assertTrue(relative1.contentLength() > 0);
|
||||||
|
assertTrue(relative1.lastModified() > 0);
|
||||||
|
|
||||||
Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class");
|
Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class");
|
||||||
assertEquals("ResourcePatternResolver.class", relative2.getFilename());
|
assertEquals("ResourcePatternResolver.class", relative2.getFilename());
|
||||||
assertTrue(relative2.getURL().getFile().endsWith("ResourcePatternResolver.class"));
|
assertTrue(relative2.getURL().getFile().endsWith("ResourcePatternResolver.class"));
|
||||||
assertTrue(relative2.exists());
|
assertTrue(relative2.exists());
|
||||||
|
assertTrue(relative2.isReadable());
|
||||||
|
assertTrue(relative2.contentLength() > 0);
|
||||||
|
assertTrue(relative2.lastModified() > 0);
|
||||||
|
|
||||||
/*
|
|
||||||
Resource relative3 = resource.createRelative("../SpringVersion.class");
|
Resource relative3 = resource.createRelative("../SpringVersion.class");
|
||||||
assertEquals("SpringVersion.class", relative3.getFilename());
|
assertEquals("SpringVersion.class", relative3.getFilename());
|
||||||
assertTrue(relative3.getURL().getFile().endsWith("SpringVersion.class"));
|
assertTrue(relative3.getURL().getFile().endsWith("SpringVersion.class"));
|
||||||
assertTrue(relative3.exists());
|
assertTrue(relative3.exists());
|
||||||
*/
|
assertTrue(relative3.isReadable());
|
||||||
|
assertTrue(relative3.contentLength() > 0);
|
||||||
|
assertTrue(relative3.lastModified() > 0);
|
||||||
|
|
||||||
|
Resource relative4 = resource.createRelative("X.class");
|
||||||
|
assertFalse(relative4.exists());
|
||||||
|
assertFalse(relative4.isReadable());
|
||||||
|
try {
|
||||||
|
relative4.contentLength();
|
||||||
|
fail("Should have thrown FileNotFoundException");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
relative4.lastModified();
|
||||||
|
fail("Should have thrown FileNotFoundException");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue