Skip to content

Commit cf3635b

Browse files
committed
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
1 parent 1e0de07 commit cf3635b

File tree

7 files changed

+138
-35
lines changed

7 files changed

+138
-35
lines changed

spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
6565
return false;
6666
}
6767
}
68-
if (con.getContentLength() >= 0) {
68+
if (con.getContentLengthLong() > 0) {
6969
return true;
7070
}
7171
if (httpCon != null) {
@@ -106,7 +106,7 @@ public boolean isReadable() {
106106
return false;
107107
}
108108
}
109-
int contentLength = con.getContentLength();
109+
long contentLength = con.getContentLengthLong();
110110
if (contentLength > 0) {
111111
return true;
112112
}
@@ -226,23 +226,35 @@ public long contentLength() throws IOException {
226226
URL url = getURL();
227227
if (ResourceUtils.isFileURL(url)) {
228228
// Proceed with file system resolution
229-
return getFile().length();
229+
File file = getFile();
230+
long length = file.length();
231+
if (length == 0L && !file.exists()) {
232+
throw new FileNotFoundException(getDescription() +
233+
" cannot be resolved in the file system for checking its content length");
234+
}
235+
return length;
230236
}
231237
else {
232238
// Try a URL connection content-length header
233239
URLConnection con = url.openConnection();
234240
customizeConnection(con);
235-
return con.getContentLength();
241+
return con.getContentLengthLong();
236242
}
237243
}
238244

239245
@Override
240246
public long lastModified() throws IOException {
241247
URL url = getURL();
248+
boolean fileCheck = false;
242249
if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) {
243250
// Proceed with file system resolution
251+
fileCheck = true;
244252
try {
245-
return super.lastModified();
253+
File fileToCheck = getFileForLastModifiedCheck();
254+
long lastModified = fileToCheck.lastModified();
255+
if (lastModified > 0L || fileToCheck.exists()) {
256+
return lastModified;
257+
}
246258
}
247259
catch (FileNotFoundException ex) {
248260
// Defensively fall back to URL connection check instead
@@ -251,7 +263,12 @@ public long lastModified() throws IOException {
251263
// Try a URL connection last-modified header
252264
URLConnection con = url.openConnection();
253265
customizeConnection(con);
254-
return con.getLastModified();
266+
long lastModified = con.getLastModified();
267+
if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) {
268+
throw new FileNotFoundException(getDescription() +
269+
" cannot be resolved in the file system for checking its last-modified timestamp");
270+
}
271+
return lastModified;
255272
}
256273

257274
/**

spring-core/src/main/java/org/springframework/core/io/AbstractResource.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public long contentLength() throws IOException {
146146
InputStream is = getInputStream();
147147
try {
148148
long size = 0;
149-
byte[] buf = new byte[255];
149+
byte[] buf = new byte[256];
150150
int read;
151151
while ((read = is.read(buf)) != -1) {
152152
size += read;
@@ -169,10 +169,11 @@ public long contentLength() throws IOException {
169169
*/
170170
@Override
171171
public long lastModified() throws IOException {
172-
long lastModified = getFileForLastModifiedCheck().lastModified();
173-
if (lastModified == 0L) {
172+
File fileToCheck = getFileForLastModifiedCheck();
173+
long lastModified = fileToCheck.lastModified();
174+
if (lastModified == 0L && !fileToCheck.exists()) {
174175
throw new FileNotFoundException(getDescription() +
175-
" cannot be resolved in the file system for resolving its last-modified timestamp");
176+
" cannot be resolved in the file system for checking its last-modified timestamp");
176177
}
177178
return lastModified;
178179
}

spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.nio.channels.FileChannel;
2727
import java.nio.channels.ReadableByteChannel;
2828
import java.nio.channels.WritableByteChannel;
29+
import java.nio.file.FileSystem;
2930
import java.nio.file.Files;
3031
import java.nio.file.NoSuchFileException;
3132
import java.nio.file.Path;
@@ -86,10 +87,10 @@ public FileSystemResource(String path) {
8687
* <p>Note: When building relative resources via {@link #createRelative},
8788
* the relative path will apply <i>at the same directory level</i>:
8889
* e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"!
89-
* If you prefer to have relative paths built underneath the given root
90-
* directory, use the {@link #FileSystemResource(String) constructor with a file path}
91-
* to append a trailing slash to the root path: "C:/dir1/", which
92-
* indicates this directory as root for all relative paths.
90+
* If you prefer to have relative paths built underneath the given root directory,
91+
* use the {@link #FileSystemResource(String) constructor with a file path}
92+
* to append a trailing slash to the root path: "C:/dir1/", which indicates
93+
* this directory as root for all relative paths.
9394
* @param file a File handle
9495
* @see #FileSystemResource(Path)
9596
* @see #getFile()
@@ -102,20 +103,38 @@ public FileSystemResource(File file) {
102103
}
103104

104105
/**
105-
* Create a new {@code FileSystemResource} from a {@link Path} handle.
106+
* Create a new {@code FileSystemResource} from a {@link Path} handle,
107+
* performing all file system interactions via NIO.2 instead of {@link File}.
106108
* <p>In contrast to {@link PathResource}, this variant strictly follows the
107109
* general {@link FileSystemResource} conventions, in particular in terms of
108110
* path cleaning and {@link #createRelative(String)} handling.
109111
* @param filePath a Path handle to a file
110112
* @since 5.1
111113
* @see #FileSystemResource(File)
112-
* @see PathResource
113114
*/
114115
public FileSystemResource(Path filePath) {
115116
Assert.notNull(filePath, "Path must not be null");
117+
this.path = StringUtils.cleanPath(filePath.toString());
118+
this.file = null;
116119
this.filePath = filePath;
120+
}
121+
122+
/**
123+
* Create a new {@code FileSystemResource} from a {@link FileSystem} handle,
124+
* locating the specified path.
125+
* <p>This is an alternative to {@link #FileSystemResource(String)},
126+
* performing all file system interactions via NIO.2 instead of {@link File}.
127+
* @param fileSystem the FileSystem to locate the path within
128+
* @param path a file path
129+
* @since 5.1.1
130+
* @see #FileSystemResource(File)
131+
*/
132+
public FileSystemResource(FileSystem fileSystem, String path) {
133+
Assert.notNull(fileSystem, "FileSystem must not be null");
134+
Assert.notNull(path, "Path must not be null");
135+
this.path = StringUtils.cleanPath(path);
117136
this.file = null;
118-
this.path = StringUtils.cleanPath(filePath.toString());
137+
this.filePath = fileSystem.getPath(this.path).normalize();
119138
}
120139

121140

@@ -240,11 +259,44 @@ public WritableByteChannel writableChannel() throws IOException {
240259
}
241260

242261
/**
243-
* This implementation returns the underlying File's length.
262+
* This implementation returns the underlying File/Path length.
244263
*/
245264
@Override
246265
public long contentLength() throws IOException {
247-
return (this.file != null ? this.file.length() : Files.size(this.filePath));
266+
if (this.file != null) {
267+
long length = this.file.length();
268+
if (length == 0L && !this.file.exists()) {
269+
throw new FileNotFoundException(getDescription() +
270+
" cannot be resolved in the file system for checking its content length");
271+
}
272+
return length;
273+
}
274+
else {
275+
try {
276+
return Files.size(this.filePath);
277+
}
278+
catch (NoSuchFileException ex) {
279+
throw new FileNotFoundException(ex.getMessage());
280+
}
281+
}
282+
}
283+
284+
/**
285+
* This implementation returns the underlying File/Path last-modified time.
286+
*/
287+
@Override
288+
public long lastModified() throws IOException {
289+
if (this.file != null) {
290+
return super.lastModified();
291+
}
292+
else {
293+
try {
294+
return Files.getLastModifiedTime(this.filePath).toMillis();
295+
}
296+
catch (NoSuchFileException ex) {
297+
throw new FileNotFoundException(ex.getMessage());
298+
}
299+
}
248300
}
249301

250302
/**
@@ -255,7 +307,8 @@ public long contentLength() throws IOException {
255307
@Override
256308
public Resource createRelative(String relativePath) {
257309
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
258-
return new FileSystemResource(pathToUse);
310+
return (this.file != null ? new FileSystemResource(pathToUse) :
311+
new FileSystemResource(this.filePath.getFileSystem(), pathToUse));
259312
}
260313

261314
/**

spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@
3737
* <p>This is the class resolved by {@link DefaultResourceLoader} for a "file:..."
3838
* URL location, allowing a downcast to {@link WritableResource} for it.
3939
*
40-
* <p>Alternatively, for direct construction from a {@link java.io.File} handle,
41-
* consider using {@link FileSystemResource}. For an NIO {@link java.nio.file.Path},
42-
* consider using {@link PathResource} instead.
40+
* <p>Alternatively, for direct construction from a {@link java.io.File} handle
41+
* or NIO {@link java.nio.file.Path}, consider using {@link FileSystemResource}.
4342
*
4443
* @author Juergen Hoeller
4544
* @since 5.0.2

spring-core/src/main/java/org/springframework/core/io/PathResource.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@
4848
* @author Philippe Marschall
4949
* @author Juergen Hoeller
5050
* @since 4.0
51-
* @see FileSystemResource#FileSystemResource(Path)
5251
* @see java.nio.file.Path
5352
* @see java.nio.file.Files
53+
* @deprecated as of 5.1.1, in favor of {@link FileSystemResource#FileSystemResource(Path)}
5454
*/
55+
@Deprecated
5556
public class PathResource extends AbstractResource implements WritableResource {
5657

5758
private final Path path;
@@ -105,7 +106,7 @@ public final String getPath() {
105106

106107
/**
107108
* This implementation returns whether the underlying file exists.
108-
* @see org.springframework.core.io.PathResource#exists()
109+
* @see java.nio.file.Files#exists(Path, java.nio.file.LinkOption...)
109110
*/
110111
@Override
111112
public boolean exists() {

spring-core/src/main/java/org/springframework/core/io/Resource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343
* @see WritableResource
4444
* @see ContextResource
4545
* @see UrlResource
46-
* @see ClassPathResource
46+
* @see FileUrlResource
4747
* @see FileSystemResource
48-
* @see PathResource
48+
* @see ClassPathResource
4949
* @see ByteArrayResource
5050
* @see InputStreamResource
5151
*/

spring-core/src/test/java/org/springframework/core/io/ResourceTests.java

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,10 @@ public void testClassPathResourceWithClass() throws IOException {
123123

124124
@Test
125125
public void testFileSystemResource() throws IOException {
126-
Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile());
126+
String file = getClass().getResource("Resource.class").getFile();
127+
Resource resource = new FileSystemResource(file);
127128
doTestResource(resource);
128-
assertEquals(new FileSystemResource(getClass().getResource("Resource.class").getFile()), resource);
129-
Resource resource2 = new FileSystemResource("core/io/Resource.class");
130-
assertEquals(resource2, new FileSystemResource("core/../core/io/./Resource.class"));
129+
assertEquals(new FileSystemResource(file), resource);
131130
}
132131

133132
@Test
@@ -136,8 +135,12 @@ public void testFileSystemResourceWithFilePath() throws Exception {
136135
Resource resource = new FileSystemResource(filePath);
137136
doTestResource(resource);
138137
assertEquals(new FileSystemResource(filePath), resource);
139-
Resource resource2 = new FileSystemResource("core/io/Resource.class");
140-
assertEquals(resource2, new FileSystemResource("core/../core/io/./Resource.class"));
138+
}
139+
140+
@Test
141+
public void testFileSystemResourceWithPlainPath() {
142+
Resource resource = new FileSystemResource("core/io/Resource.class");
143+
assertEquals(resource, new FileSystemResource("core/../core/io/./Resource.class"));
141144
}
142145

143146
@Test
@@ -157,23 +160,52 @@ public void testUrlResource() throws IOException {
157160
private void doTestResource(Resource resource) throws IOException {
158161
assertEquals("Resource.class", resource.getFilename());
159162
assertTrue(resource.getURL().getFile().endsWith("Resource.class"));
163+
assertTrue(resource.exists());
164+
assertTrue(resource.isReadable());
165+
assertTrue(resource.contentLength() > 0);
166+
assertTrue(resource.lastModified() > 0);
160167

161168
Resource relative1 = resource.createRelative("ClassPathResource.class");
162169
assertEquals("ClassPathResource.class", relative1.getFilename());
163170
assertTrue(relative1.getURL().getFile().endsWith("ClassPathResource.class"));
164171
assertTrue(relative1.exists());
172+
assertTrue(relative1.isReadable());
173+
assertTrue(relative1.contentLength() > 0);
174+
assertTrue(relative1.lastModified() > 0);
165175

166176
Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class");
167177
assertEquals("ResourcePatternResolver.class", relative2.getFilename());
168178
assertTrue(relative2.getURL().getFile().endsWith("ResourcePatternResolver.class"));
169179
assertTrue(relative2.exists());
180+
assertTrue(relative2.isReadable());
181+
assertTrue(relative2.contentLength() > 0);
182+
assertTrue(relative2.lastModified() > 0);
170183

171-
/*
172184
Resource relative3 = resource.createRelative("../SpringVersion.class");
173185
assertEquals("SpringVersion.class", relative3.getFilename());
174186
assertTrue(relative3.getURL().getFile().endsWith("SpringVersion.class"));
175187
assertTrue(relative3.exists());
176-
*/
188+
assertTrue(relative3.isReadable());
189+
assertTrue(relative3.contentLength() > 0);
190+
assertTrue(relative3.lastModified() > 0);
191+
192+
Resource relative4 = resource.createRelative("X.class");
193+
assertFalse(relative4.exists());
194+
assertFalse(relative4.isReadable());
195+
try {
196+
relative4.contentLength();
197+
fail("Should have thrown FileNotFoundException");
198+
}
199+
catch (FileNotFoundException ex) {
200+
// expected
201+
}
202+
try {
203+
relative4.lastModified();
204+
fail("Should have thrown FileNotFoundException");
205+
}
206+
catch (FileNotFoundException ex) {
207+
// expected
208+
}
177209
}
178210

179211
@Test

0 commit comments

Comments
 (0)