22
22
import java .io .InputStream ;
23
23
import java .nio .file .Files ;
24
24
import java .nio .file .StandardCopyOption ;
25
+ import java .util .HashMap ;
26
+ import java .util .Locale ;
27
+ import java .util .Map ;
25
28
import java .util .Set ;
26
29
import java .util .jar .Manifest ;
27
30
28
31
import org .apache .catalina .LifecycleException ;
29
32
import org .apache .catalina .WebResource ;
33
+ import org .apache .catalina .WebResourceLockSet ;
30
34
import org .apache .catalina .WebResourceRoot ;
31
35
import org .apache .catalina .WebResourceRoot .ResourceSetType ;
32
36
import org .apache .catalina .util .ResourceSet ;
33
37
import org .apache .juli .logging .Log ;
34
38
import org .apache .juli .logging .LogFactory ;
39
+ import org .apache .tomcat .util .http .RequestUtil ;
35
40
36
41
/**
37
42
* Represents a {@link org.apache.catalina.WebResourceSet} based on a directory.
38
43
*/
39
- public class DirResourceSet extends AbstractFileResourceSet {
44
+ public class DirResourceSet extends AbstractFileResourceSet implements WebResourceLockSet {
40
45
41
46
private static final Log log = LogFactory .getLog (DirResourceSet .class );
42
47
48
+ private boolean caseSensitive = true ;
49
+
50
+ private Map <String ,ResourceLock > resourceLocksByPath = new HashMap <>();
51
+ private Object resourceLocksByPathLock = new Object ();
52
+
53
+
43
54
/**
44
55
* A no argument constructor is required for this to work with the digester.
45
56
*/
@@ -91,22 +102,33 @@ public WebResource getResource(String path) {
91
102
String webAppMount = getWebAppMount ();
92
103
WebResourceRoot root = getRoot ();
93
104
if (path .startsWith (webAppMount )) {
94
- File f = file (path .substring (webAppMount .length ()), false );
95
- if (f == null ) {
96
- return new EmptyResource (root , path );
97
- }
98
- if (!f .exists ()) {
99
- return new EmptyResource (root , path , f );
100
- }
101
- if (f .isDirectory () && path .charAt (path .length () - 1 ) != '/' ) {
102
- path = path + '/' ;
105
+ /*
106
+ * Lock the path for reading until the WebResource has been constructed. The lock prevents concurrent reads
107
+ * and writes (e.g. HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource
108
+ * where some of the fields are set as if the file exists and some as set as if it does not.
109
+ */
110
+ ResourceLock lock = lockForRead (path );
111
+ try {
112
+ File f = file (path .substring (webAppMount .length ()), false );
113
+ if (f == null ) {
114
+ return new EmptyResource (root , path );
115
+ }
116
+ if (!f .exists ()) {
117
+ return new EmptyResource (root , path , f );
118
+ }
119
+ if (f .isDirectory () && path .charAt (path .length () - 1 ) != '/' ) {
120
+ path = path + '/' ;
121
+ }
122
+ return new FileResource (root , path , f , isReadOnly (), getManifest (), this , lock .key );
123
+ } finally {
124
+ unlockForRead (lock );
103
125
}
104
- return new FileResource (root , path , f , isReadOnly (), getManifest ());
105
126
} else {
106
127
return new EmptyResource (root , path );
107
128
}
108
129
}
109
130
131
+
110
132
@ Override
111
133
public String [] list (String path ) {
112
134
checkPath (path );
@@ -246,32 +268,42 @@ public boolean write(String path, InputStream is, boolean overwrite) {
246
268
return false ;
247
269
}
248
270
249
- File dest = null ;
250
271
String webAppMount = getWebAppMount ();
251
- if (path .startsWith (webAppMount )) {
272
+ if (!path .startsWith (webAppMount )) {
273
+ return false ;
274
+ }
275
+
276
+ File dest = null ;
277
+ /*
278
+ * Lock the path for writing until the write is complete. The lock prevents concurrent reads and writes (e.g.
279
+ * HTTP GET and PUT / DELETE) for the same path causing corruption of the FileResource where some of the fields
280
+ * are set as if the file exists and some as set as if it does not.
281
+ */
282
+ ResourceLock lock = lockForWrite (path );
283
+ try {
252
284
dest = file (path .substring (webAppMount .length ()), false );
253
285
if (dest == null ) {
254
286
return false ;
255
287
}
256
- } else {
257
- return false ;
258
- }
259
288
260
- if (dest .exists () && !overwrite ) {
261
- return false ;
262
- }
289
+ if (dest .exists () && !overwrite ) {
290
+ return false ;
291
+ }
263
292
264
- try {
265
- if (overwrite ) {
266
- Files .copy (is , dest .toPath (), StandardCopyOption .REPLACE_EXISTING );
267
- } else {
268
- Files .copy (is , dest .toPath ());
293
+ try {
294
+ if (overwrite ) {
295
+ Files .copy (is , dest .toPath (), StandardCopyOption .REPLACE_EXISTING );
296
+ } else {
297
+ Files .copy (is , dest .toPath ());
298
+ }
299
+ } catch (IOException ioe ) {
300
+ return false ;
269
301
}
270
- } catch (IOException ioe ) {
271
- return false ;
272
- }
273
302
274
- return true ;
303
+ return true ;
304
+ } finally {
305
+ unlockForWrite (lock );
306
+ }
275
307
}
276
308
277
309
@ Override
@@ -286,6 +318,7 @@ protected void checkType(File file) {
286
318
@ Override
287
319
protected void initInternal () throws LifecycleException {
288
320
super .initInternal ();
321
+ caseSensitive = isCaseSensitive ();
289
322
// Is this an exploded web application?
290
323
if (getWebAppMount ().equals ("" )) {
291
324
// Look for a manifest
@@ -299,4 +332,114 @@ protected void initInternal() throws LifecycleException {
299
332
}
300
333
}
301
334
}
335
+
336
+
337
+ /*
338
+ * Determines if this ResourceSet is based on a case sensitive file system or not.
339
+ */
340
+ private boolean isCaseSensitive () {
341
+ try {
342
+ String canonicalPath = getFileBase ().getCanonicalPath ();
343
+ File upper = new File (canonicalPath .toUpperCase (Locale .ENGLISH ));
344
+ if (!canonicalPath .equals (upper .getCanonicalPath ())) {
345
+ return true ;
346
+ }
347
+ File lower = new File (canonicalPath .toLowerCase (Locale .ENGLISH ));
348
+ if (!canonicalPath .equals (lower .getCanonicalPath ())) {
349
+ return true ;
350
+ }
351
+ /*
352
+ * Both upper and lower case versions of the current fileBase have the same canonical path so the file
353
+ * system must be case insensitive.
354
+ */
355
+ } catch (IOException ioe ) {
356
+ log .warn (sm .getString ("dirResourceSet.isCaseSensitive.fail" , getFileBase ().getAbsolutePath ()), ioe );
357
+ }
358
+
359
+ return false ;
360
+ }
361
+
362
+
363
+ private String getLockKey (String path ) {
364
+ // Normalize path to ensure that the same key is used for the same path.
365
+ String normalisedPath = RequestUtil .normalize (path );
366
+ if (caseSensitive ) {
367
+ return normalisedPath ;
368
+ }
369
+ return normalisedPath .toLowerCase (Locale .ENGLISH );
370
+ }
371
+
372
+
373
+ @ Override
374
+ public ResourceLock lockForRead (String path ) {
375
+ String key = getLockKey (path );
376
+ ResourceLock resourceLock = null ;
377
+ synchronized (resourceLocksByPathLock ) {
378
+ /*
379
+ * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
380
+ * a consistent view of the currently "in-use" ResourceLocks.
381
+ */
382
+ resourceLock = resourceLocksByPath .get (key );
383
+ if (resourceLock == null ) {
384
+ resourceLock = new ResourceLock (key );
385
+ }
386
+ resourceLock .count .incrementAndGet ();
387
+ }
388
+ // Obtain the lock outside the sync as it will block if there is a current write lock.
389
+ resourceLock .reentrantLock .readLock ().lock ();
390
+ return resourceLock ;
391
+ }
392
+
393
+
394
+ @ Override
395
+ public void unlockForRead (ResourceLock resourceLock ) {
396
+ // Unlock outside the sync as there is no need to do it inside.
397
+ resourceLock .reentrantLock .readLock ().unlock ();
398
+ synchronized (resourceLocksByPathLock ) {
399
+ /*
400
+ * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
401
+ * map always has a consistent view of the currently "in-use" ResourceLocks.
402
+ */
403
+ if (resourceLock .count .decrementAndGet () == 0 ) {
404
+ resourceLocksByPath .remove (resourceLock .key );
405
+ }
406
+ }
407
+ }
408
+
409
+
410
+ @ Override
411
+ public ResourceLock lockForWrite (String path ) {
412
+ String key = getLockKey (path );
413
+ ResourceLock resourceLock = null ;
414
+ synchronized (resourceLocksByPathLock ) {
415
+ /*
416
+ * Obtain the ResourceLock and increment the usage count inside the sync to ensure that that map always has
417
+ * a consistent view of the currently "in-use" ResourceLocks.
418
+ */
419
+ resourceLock = resourceLocksByPath .get (key );
420
+ if (resourceLock == null ) {
421
+ resourceLock = new ResourceLock (key );
422
+ }
423
+ resourceLock .count .incrementAndGet ();
424
+ }
425
+ // Obtain the lock outside the sync as it will block if there are any other current locks.
426
+ resourceLock .reentrantLock .writeLock ().lock ();
427
+ return resourceLock ;
428
+ }
429
+
430
+
431
+ @ Override
432
+ public void unlockForWrite (ResourceLock resourceLock ) {
433
+ // Unlock outside the sync as there is no need to do it inside.
434
+ resourceLock .reentrantLock .writeLock ().unlock ();
435
+ synchronized (resourceLocksByPathLock ) {
436
+ /*
437
+ * Decrement the usage count and remove ResourceLocks no longer required inside the sync to ensure that that
438
+ * map always has a consistent view of the currently "in-use" ResourceLocks.
439
+ */
440
+ if (resourceLock .count .decrementAndGet () == 0 ) {
441
+ resourceLocksByPath .remove (resourceLock .key );
442
+ }
443
+ }
444
+ }
302
445
}
0 commit comments