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:
Juergen Hoeller 2018-10-09 23:13:11 +02:00
parent 1e0de072f9
commit cf3635b42d
7 changed files with 139 additions and 36 deletions

View File

@ -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;
} }
/** /**

View File

@ -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;
} }

View File

@ -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));
} }
/** /**

View File

@ -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

View File

@ -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() {

View File

@ -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
*/ */

View File

@ -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